¿Cuál es la palabra clave `some` en SwiftUI?
swift5.1 (5)
El "algunos" significa:
" Esta nueva palabra clave Swift 5.1 significa que la propiedad calculada puede devolver cualquier cosa cuando al menos se ajusta al protocolo View. Lo que es cierto para el texto. Y con Swift 5.1 tampoco tenemos que agregar más la palabra clave return. La última línea de una función o cierre será devuelta automáticamente " .
El nuevo tutorial de SwiftUI tiene el siguiente código:
struct ContentView: View {
var body: some View {
Text("Hello World")
}
}
La segunda línea de la palabra
some
, y en su sitio se resalta como si fuera una palabra clave.
Swift 5.1 no parece tener
some
como palabra clave, y no veo qué otra palabra podría estar haciendo allí, ya que va donde el tipo normalmente va.
¿Hay una nueva versión sin previo aviso de Swift?
¿Es una función que se está utilizando en un tipo de una forma que no conocía?
¿Qué hace la palabra clave
some
?
La otra respuesta hace un buen trabajo al explicar el aspecto técnico de la nueva palabra clave, pero esta respuesta intentará explicar fácilmente por qué .
Digamos que tengo un protocolo Animal y quiero comparar si dos animales son hermanos:
protocol Animal {
func isSibling(with animal: Self) -> Bool
}
De esta manera, solo tiene sentido comparar si dos animales son hermanos si son del mismo tipo de animal.
Ahora déjame crear un ejemplo de un animal solo como referencia
class Dog: Animal {
func isSibling(with animal: Dog) -> Bool {
return true // doesn''t really matter implementation of this
}
}
El camino sin un
some T
Ahora digamos que tengo una función que devuelve un animal de una "familia".
func animalFromAnimalFamily() -> Animal {
return myDog // myDog is just some random variable of type `Dog`
}
Nota: esta función no compilará realmente. Esto se debe a que antes de que se agregara la característica ''algunos'', no puede devolver un tipo de protocolo si el protocolo usa ''Self'' o genéricos . Pero digamos que puedes ... simular este upcasts myDog para el tipo abstracto Animal, veamos qué sucede
Ahora el problema viene es si intento hacer esto:
let animal1: Animal = animalFromAnimalFamily()
let animal2: Animal = animalFromAnimalFamily()
animal1.isSibling(animal2) // error
Esto arrojará un error .
¿Por qué?
Bueno, la razón es que cuando llamas
animal1.isSibling(animal2)
Swift no sabe si los animales son perros, gatos o lo que sea.
Por lo que Swift sabe,
animal1
y
animal2
podrían ser especies animales no relacionadas
.
Ya que no podemos comparar animales de diferentes tipos (ver arriba).
Este error
Como
some T
resuelven este problema.
Vamos a reescribir la función anterior:
func animalFromAnimalFamily() -> some Animal {
return myDog
}
let animal1 = animalFromAnimalFamily()
let animal2 = animalFromAnimalFamily()
animal1.isSibling(animal2)
animal1
y
animal2
no
son
Animal
,
pero
son clases que implementan animales
.
Lo que esto te permite hacer ahora es cuando llamas
animal1.isSibling(animal2)
, Swift sabe que
animal1
y
animal2
son del mismo tipo.
Así que la forma en que me gusta pensarlo:
some T
permite a Swift saber qué implementación deT
se está utilizando, pero el usuario de la clase no.
(Descargo de responsabilidad de autopromoción) He escrito una publicación de blog que profundiza un poco más (el mismo ejemplo que aquí) en esta nueva característica.
La palabra clave de Swift 5.1 ( SE-0244 ) se utiliza junto con un protocolo como tipo de retorno.
Las notas de lanzamiento de Xcode 11 lo presentan así:
Las funciones ahora pueden ocultar su tipo de retorno concreto declarando qué protocolos cumple, en lugar de especificar el tipo de retorno exacto:
func makeACollection() -> some Collection { return [1, 2, 3] }
El código que llama a la función puede usar la interfaz del protocolo, pero no tiene visibilidad del tipo subyacente. ( SE-0244 , 40538331)
En el ejemplo anterior, no necesita decir que va a devolver un
Array
.
Eso le permite incluso devolver un tipo genérico que solo se ajusta a la
Collection
.
Ahora,
hay un
problema conocido
notable
al usar
some
:
Las declaraciones con algunos tipos de retorno de protocolo requieren el tiempo de ejecución Swift 5.1 en iOS 13, macOS 10.15, watchOS 6 o tvOS 13, pero el compilador Swift no lo aplica. La ejecución de una aplicación que utiliza algunos tipos de devolución en versiones anteriores del sistema operativo puede bloquearse en el tiempo de ejecución, ya sea porque faltan los símbolos swift_getOpaqueTypeMetadata, o por un error al desmarcar una cadena que contiene la subcadena "Qo_". (50731151)
Solución alternativa : solo implemente archivos binarios que utilicen algunos tipos de retorno a iOS 13, macOS 10.15, watchOS 6 y tvOS 13. Evítelos en el código que debe ejecutarse en versiones anteriores del sistema operativo.
Así que significa que se supone que debes usar la disponibilidad para evitar
some
en iOS 12 y antes:
@available(iOS 13.0, *)
func makeACollection() -> some Collection {
...
}
La respuesta de Hamish
es bastante impresionante y responde a la pregunta desde una perspectiva técnica.
Me gustaría agregar algunas ideas sobre por qué se usa la palabra clave
some
en este lugar en particular en los
tutoriales de SwiftUI
de Apple y por qué es una buena práctica seguir.
some
no son un requisito!
En primer lugar, no es
necesario
declarar el tipo de devolución del
body
como un tipo opaco.
Siempre puede devolver el tipo concreto en lugar de usar la
some View
.
struct ContentView: View {
var body: Text {
Text("Hello World")
}
}
Esto compilará también.
Cuando mire a la interfaz de la
View
, verá que el tipo de
body
de retorno es un tipo asociado:
public protocol View : _View {
/// The type of view representing the body of this view.
///
/// When you create a custom view, Swift infers this type from your
/// implementation of the required `body` property.
associatedtype Body : View
/// Declares the content and behavior of this view.
var body: Self.Body { get }
}
Esto significa que especifica este tipo anotando la propiedad del
body
con un tipo particular de su elección.
El único requisito es que este tipo necesita implementar el protocolo
View
sí.
Eso puede ser un tipo
específico
que implemente
View
, por ejemplo
-
Text
-
Image
-
Circle
- ...
o un tipo
opaco
que implementa la
View
, es decir,
-
some View
Vistas genéricas
El problema surge cuando intentamos usar una vista de pila como el tipo de retorno del
body
, como
VStack
o
HStack
:
struct ContentView: View {
var body: VStack {
VStack {
Text("Hello World")
Image(systemName: "video.fill")
}
}
}
Esto no se compilará y obtendrá el error:
La referencia al tipo genérico ''VStack'' requiere argumentos en <...>
¡Eso es porque las vistas de pila en SwiftUI son tipos genéricos ! 💡 (Y lo mismo se aplica a las listas y otros tipos de vista de contenedor).
Eso tiene mucho sentido porque puede conectar cualquier número de vistas de cualquier tipo (siempre que se ajuste al protocolo de
View
).
El tipo concreto de la
VStack
en el cuerpo de arriba es en realidad
VStack<TupleView<(Text, Image)>>
Cuando más tarde decidamos agregar una vista a la pila, su tipo concreto cambia. Si agregamos un segundo texto después del primero, obtenemos
VStack<TupleView<(Text, Text, Image)>>
Incluso si hacemos un cambio menor, algo tan sutil como agregar un espaciador entre el texto y la imagen, el tipo de pila cambia:
VStack<TupleView<(Text, _ModifiedContent<Spacer, _FrameLayout>, Image)>>
Por lo que puedo decir,
esa es
la razón por la que Apple recomienda en sus tutoriales usar siempre
some View
, el tipo opaco más general que satisfacen todas las vistas, como el tipo de devolución del
body
.
Puede cambiar la implementación / el diseño de su vista personalizada sin cambiar manualmente el tipo de retorno cada vez.
Suplemento:
Si desea obtener una comprensión más intuitiva de los tipos de resultados opacos, recientemente publiqué un artículo que podría valer la pena leer:
🔗 ¿Qué es esto "algunos" en SwiftUI?
some View
es
un tipo de resultado opaco
como lo presentó el
SE-0244
y está disponible en Swift 5.1 con Xcode 11. Puede pensar que esto es un marcador de posición genérico "inverso".
A diferencia de un marcador de posición genérico regular que satisface la persona que llama:
protocol P {}
struct S1 : P {}
struct S2 : P {}
func foo<T : P>(_ x: T) {}
foo(S1()) // Caller chooses T == S1.
foo(S2()) // Caller chooses T == S2.
Un tipo de resultado opaco es un marcador de posición genérico implícito que satisface la implementación , por lo que puede pensar en esto:
func bar() -> some P {
return S1() // Implementation chooses S1 for the opaque result.
}
Como se ve así:
func bar() -> <Output : P> Output {
return S1() // Implementation chooses Output == S1.
}
De hecho, el objetivo final con esta característica es permitir genéricos inversos en esta forma más explícita, lo que también le permitiría agregar restricciones, por ejemplo,
-> <T : Collection> T where T.Element == Int
.
Ver esta publicación para más información
.
Lo principal que se debe sacar de esto es que una función que devuelve
some P
es aquella que devuelve un valor de un tipo concreto específico que se ajusta a
P
Intentar devolver diferentes tipos conformes dentro de la función produce un error de compilación:
// error: Function declares an opaque return type, but the return
// statements in its body do not have matching underlying types.
func bar(_ x: Int) -> some P {
if x > 10 {
return S1()
} else {
return S2()
}
}
Como el marcador de posición genérico implícito no puede ser satisfecho por varios tipos.
Esto contrasta con una función que devuelve
P
, que se puede usar para representar
S1
y
S2
porque representa un valor de conformación arbitrario de
P
:
func baz(_ x: Int) -> P {
if x > 10 {
return S1()
} else {
return S2()
}
}
Bien, entonces, ¿qué beneficios tienen los tipos de resultados opacos
-> some P
tienen sobre tipos de retorno de protocolo
-> P
?
1. Los tipos de resultados opacos se pueden utilizar con PAT
Una importante limitación actual de los protocolos es que los PAT (protocolos con tipos asociados) no pueden utilizarse como tipos reales. Aunque esta es una restricción que probablemente se eliminará en una versión futura del lenguaje, debido a que los tipos de resultados opacos son en realidad solo marcadores de posición genéricos, se pueden usar con los PAT de hoy.
Esto significa que puedes hacer cosas como:
func giveMeACollection() -> some Collection {
return [1, 2, 3]
}
let collection = giveMeACollection()
print(collection.count) // 3
2. Los tipos de resultados opacos tienen identidad.
Debido a que los tipos de resultados opacos hacen que se devuelva un solo tipo concreto, el compilador sabe que dos llamadas a la misma función deben devolver dos valores del mismo tipo.
Esto significa que puedes hacer cosas como:
// foo() -> <Output : Equatable> Output {
func foo() -> some Equatable {
return 5 // The opaque result type is inferred to be Int.
}
let x = foo()
let y = foo()
print(x == y) // Legal both x and y have the return type of foo.
Esto es legal porque el compilador sabe que tanto
x
como
y
tienen el mismo tipo concreto.
Este es un requisito importante para
==
, donde ambos parámetros son del tipo
Self
.
protocol Equatable {
static func == (lhs: Self, rhs: Self) -> Bool
}
Esto significa que espera dos valores que sean del mismo tipo que el tipo de conformidad concreto.
Incluso si
Equatable
fuera utilizable como un tipo, no podría comparar dos valores conformes de
Equatable
arbitrarios entre sí, por ejemplo:
func foo(_ x: Int) -> Equatable { // Assume this is legal.
if x > 10 {
return 0
} else {
return "hello world"
}
}
let x = foo(20)
let y = foo(5)
print(x == y) // Illegal.
Como el compilador no puede probar que dos valores
Equatable
arbitrarios tienen el mismo tipo concreto subyacente.
De manera similar, si introducimos otra función de retorno de tipo opaco:
// foo() -> <Output1 : Equatable> Output1 {
func foo() -> some Equatable {
return 5 // The opaque result type is inferred to be Int.
}
// bar() -> <Output2 : Equatable> Output2 {
func bar() -> some Equatable {
return "" // The opaque result type is inferred to be String.
}
let x = foo()
let y = bar()
print(x == y) // Illegal, the return type of foo != return type of bar.
El ejemplo se convierte en ilegal porque, aunque tanto
foo
como
bar
devuelven
some Equatable
, sus marcadores de posición genéricos "inversos"
Output1
y
Output1
podrían satisfacerse con diferentes tipos.
3. Los tipos de resultados opacos se componen con marcadores de posición genéricos
A diferencia de los valores de tipo de protocolo regulares, los tipos de resultados opacos se componen bien con los marcadores de posición genéricos regulares, por ejemplo:
protocol P {
var i: Int { get }
}
struct S : P {
var i: Int
}
func makeP() -> some P { // Opaque result type inferred to be S.
return S(i: .random(in: 0 ..< 10))
}
func bar<T : P>(_ x: T, _ y: T) -> T {
return x.i < y.i ? x : y
}
let p1 = makeP()
let p2 = makeP()
print(bar(p1, p2)) // Legal, T is inferred to be the return type of makeP.
Esto no hubiera funcionado si
makeP
hubiera devuelto
P
, ya que dos valores de
P
pueden tener diferentes tipos concretos subyacentes, por ejemplo:
struct T : P {
var i: Int
}
func makeP() -> P {
if .random() { // 50:50 chance of picking each branch.
return S(i: 0)
} else {
return T(i: 1)
}
}
let p1 = makeP()
let p2 = makeP()
print(bar(p1, p2)) // Illegal.
¿Por qué usar un tipo de resultado opaco sobre el tipo de concreto?
En este punto, puedes estar pensando en ti mismo, ¿por qué no solo escribes el código como:
func makeP() -> S {
return S(i: 0)
}
Bueno, el uso de un tipo de resultado opaco le permite hacer del tipo
S
un detalle de implementación exponiendo solo la interfaz proporcionada por
P
, lo que le brinda la flexibilidad de cambiar el tipo concreto más adelante en la línea sin romper ningún código que dependa de la función.
Por ejemplo, podría reemplazar:
func makeP() -> some P {
return S(i: 0)
}
con:
func makeP() -> some P {
return T(i: 1)
}
sin romper ningún código que llame a
makeP()
.
Consulte la sección Tipos opacos de la guía de idiomas y SE-0244 para obtener más información sobre esta función.