¿Cuál es la palabra clave `some` en Swift (UI)?

259

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 someuna palabra clave, y no veo qué otra cosa somepodría estar haciendo la palabra allí, ya que va donde suele ir el tipo. ¿Existe una nueva versión no anunciada de Swift? ¿Es una función que se usa en un tipo de una manera que no conocía?

¿Qué hace la palabra clave some?

Nicholas
fuente
Para aquellos que estaban mareados por el tema, aquí un artículo muy descifrado y paso a paso gracias a Vadim Bulavin. vadimbulavin.com/…
Luc-Olivier

Respuestas:

333

some Viewes un tipo de resultado opaco introducido por SE-0244 y está disponible en Swift 5.1 con Xcode 11. Puede pensar en esto como 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 satisfecho por 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 función 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. Vea esta publicación para más información .

Lo principal que debemos sacar de esto es que una función que devuelve some Pes aquella que devuelve un valor de un tipo concreto único 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 múltiples tipos.

Esto contrasta con el retorno de una función P, que se puede usar para representar ambos S1 y S2porque representa un Pvalor conforme arbitrario :

func baz(_ x: Int) -> P {
  if x > 10 {
    return S1()
  } else {
    return S2()
  }
}

Bien, entonces, ¿qué beneficios tienen los tipos de resultados opacos -> some Psobre los tipos de retorno de protocolo -> P?


1. Los tipos de resultados opacos se pueden usar con PAT

Una limitación importante actual de los protocolos es que los PAT (protocolos con tipos asociados) no pueden usarse 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 efectivamente marcadores de posición genéricos, hoy en día se pueden usar con PAT.

Esto significa que puede 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 obligan a 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 puede 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 ambos xy ytienen el mismo tipo concreto. Este es un requisito importante para ==, donde ambos parámetros de 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 conformación concreta. Incluso si Equatablefuera utilizable como tipo, no podría comparar dos Equatablevalores conformes 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 Equatablevalores arbitrarios tienen el mismo tipo concreto subyacente.

De manera similar, si presentamos 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 vuelve ilegal porque aunque ambos fooy bardevuelven some Equatable, sus marcadores de posición genéricos "inversos" Output1y Output2podrían ser satisfechos por diferentes tipos.


3. Los tipos de resultados opacos se componen con marcadores genéricos

A diferencia de los valores normales de tipo protocolo, los tipos de resultados opacos se componen bien con 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 habría funcionado si makePhubiera regresado P, ya que dos Pvalores pueden tener diferentes tipos de hormigón 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 concreto?

En este punto, puede estar pensando para usted mismo, ¿por qué no simplemente escribir el código como:

func makeP() -> S {
  return S(i: 0)
}

Bueno, el uso de un tipo de resultado opaco le permite convertir el tipo en Sun detalle de implementación al exponer solo la interfaz proporcionada por P, dándole flexibilidad para 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 makeP().

Consulte la sección Tipos opacos de la guía de idiomas y la propuesta de evolución rápida para obtener más información sobre esta función.

Hamish
fuente
20
Sin relación: A partir de Swift 5.1, returnno se requiere en las funciones de una sola expresión
ielyamani
3
Pero, ¿cuál es la diferencia entre: func makeP() -> some Py func makeP() -> P? He leído la propuesta, y no puedo ver esta diferencia para sus muestras también.
Artem
2
El manejo del tipo Swifts es un desastre. ¿Es esta especificidad realmente algo que no se puede manejar en tiempo de compilación? Vea C # para referencia, maneja todos estos casos implícitamente a través de una sintaxis simple. Los veloces deben tener una sintaxis explícitamente poco explícita de cultistas de carga que realmente ofusca el lenguaje. ¿Puede explicar también la razón del diseño para esto, por favor? (Si tiene un enlace a la propuesta en github, eso también sería bueno) Editar: Acabo de notar que está vinculado en la parte superior.
SacredGeometry
2
@Zmaster El compilador tratará dos tipos de retorno opacos como diferentes incluso si la implementación para ambos devuelve el mismo tipo concreto. En otras palabras, el tipo de concreto específico elegido está oculto para la persona que llama. (He tenido la intención de ampliar la segunda mitad de mi respuesta para hacer que este tipo de cosas sea un poco más explícito, pero aún no lo he hecho).
Hamish
52

La otra respuesta hace un buen trabajo al explicar el aspecto técnico de la nueva somepalabra 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(_ animal: Self) -> Bool
}

De esta manera, solo tiene sentido comparar si dos animales son hermanos si son del mismo tipo de animal.


Ahora déjenme crear un ejemplo de un animal solo como referencia

class Dog: Animal {
    func isSibling(_ animal: Dog) -> Bool {
        return true // doesn't really matter implementation of this
    }
}

El camino sin 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 se compilará realmente. Esto se debe a que antes de agregar la función 'algunos' no puede devolver un tipo de protocolo si el protocolo usa 'Self' o genéricos . Pero digamos que puedes ... pretendiendo que esto emita myDog al tipo abstracto Animal, veamos qué sucede

Ahora surge el problema si trato de 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 a animal1.isSibling(animal2)Swift no sabes si los animales son perros, gatos o lo que sea. Hasta donde Swift sabe, animal1y animal2podrían ser especies animales no relacionadas . Ya que no podemos comparar animales de diferentes tipos (ver arriba). Esto producirá un error

¿Cómo some Tresuelve este problema?

Reescribamos la función anterior:

func animalFromAnimalFamily() -> some Animal {
    return myDog
}
let animal1 = animalFromAnimalFamily()
let animal2 = animalFromAnimalFamily()

animal1.isSibling(animal2)

animal1y no loanimal2 son , pero son una clase que implementa Animal . Animal

Lo que esto te permite hacer ahora es cuando llamas animal1.isSibling(animal2), Swift lo sabe animal1y animal2son del mismo tipo.

Entonces, la forma en que me gusta pensarlo:

some Tle permite a Swift saber qué implementación Tse 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í) sobre esta nueva función

Downgoat
fuente
2
Entonces, ¿su idea es que la persona que llama puede aprovechar el hecho de que dos llamadas a la función devuelven el mismo tipo aunque la persona que llama no sepa de qué tipo es?
mate
1
@matt esencialmente sí. El mismo concepto cuando se usa con campos, etc., la persona que llama tiene la garantía de que el tipo de retorno siempre será del mismo tipo, pero no revela exactamente cuál es el tipo.
Downgoat
@Downgoat muchas gracias por una publicación y respuesta perfectas. Como entendí someen el tipo de retorno, funciona como una restricción al cuerpo de la función. Por lo tanto, somerequiere devolver solo un tipo concreto en todo el cuerpo de la función. Por ejemplo: si existe return randomDog, todas las demás devoluciones solo deben funcionar con Dog. Todos los beneficios provienen de esta restricción: disponibilidad animal1.isSibling(animal2)y beneficio de la compilación de func animalFromAnimalFamily() -> some Animal(porque ahora Selfse define bajo el capó). ¿Es correcto?
Artem
55
Esta línea era todo lo que necesitaba, animal1 y animal2 no son animales, pero son una clase que implementa Animal, ¡ahora todo tiene sentido!
aross
29

La respuesta de Hamish es bastante impresionante y responde la pregunta desde una perspectiva técnica. Me gustaría agregar algunas ideas sobre por qué la palabra clave somese usa en este lugar en particular en los tutoriales SwiftUI de Apple y por qué es una buena práctica seguirla.

some No es un requisito!

En primer lugar, no necesita declarar el bodytipo de retorno del tipo como un tipo opaco. Siempre puede devolver el tipo concreto en lugar de usar el some View.

struct ContentView: View {
    var body: Text {
        Text("Hello World")
    }
}

Esto también se compilará. Cuando observa la Viewinterfaz de 's, verá que el tipo de retorno de bodyes 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 usted especifica este tipo al anotar la bodypropiedad con un tipo particular de su elección. El único requisito es que este tipo necesita implementar el Viewprotocolo en sí.

Ese puede ser un tipo específico que implementa View, por ejemplo

  • Text
  • Image
  • Circle
  • ...

o un tipo opaco que implementa View, es decir

  • some View

Vistas genéricas

El problema surge cuando intentamos usar una vista de pila como el bodytipo de retorno del tipo, como VStacko 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 <...>

¡Esto se debe a que las vistas de pila en SwiftUI son tipos genéricos ! 💡 (Y lo mismo es cierto para las Listas y otros tipos de vista de contenedor).

Eso tiene mucho sentido porque puede conectar cualquier cantidad de vistas de cualquier tipo (siempre que se ajuste al Viewprotocolo). El tipo concreto de VStacken el cuerpo de arriba es en realidad

VStack<TupleView<(Text, Image)>>

Cuando más tarde decidimos 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 cual Apple recomienda en sus tutoriales usar siempre some View, el tipo opaco más general que satisfacen todas las vistas, como el bodytipo de retorno del. 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 "algo" en SwiftUI?

Mischa
fuente
2
Esta. ¡Gracias! La respuesta de Hamish fue muy completa, pero la suya me dice exactamente por qué se usa en estos ejemplos.
Chris Marshall
Me encanta la idea de "algunos". ¿Alguna idea de si usar "algunos" afecta el tiempo de compilación?
Tofu Warrior
@Mischa, entonces, ¿cómo hacer vistas genéricas? con un protocolo que contiene puntos de vista y otros comportamientos?
theMouk
27

Creo que todas las respuestas que faltan hasta ahora es que somees útil principalmente en algo como un DSL (lenguaje específico de dominio) como SwiftUI o una biblioteca / marco, que tendrá usuarios (otros programadores) diferentes de usted.

Probablemente nunca usarías some en su código de aplicación normal, excepto tal vez en la medida en que pueda envolver un protocolo genérico para que pueda usarse como un tipo (en lugar de solo como una restricción de tipo). Lo que somehace es dejar que el compilador sepa qué tipo específico es algo, mientras coloca una fachada de supertipo frente a él.

Por lo tanto, en SwiftUI, donde usted es el usuario, todos que necesita saber es que algo es así some View, mientras que detrás de escena puede continuar todo tipo de pañuelos de los que está protegido. Este objeto es, de hecho, un tipo muy específico, pero nunca necesitará saber de qué se trata. Sin embargo, a diferencia de un protocolo, es un tipo completo, porque donde quiera que aparezca es simplemente una fachada para algún tipo específico de pleno derecho.

En una versión futura de SwiftUI, donde espera un some View , los desarrolladores podrían cambiar el tipo subyacente de ese objeto en particular. Pero eso no romperá su código, porque su código nunca mencionó el tipo subyacente en primer lugar.

Por lo tanto, someen efecto, hace que un protocolo se parezca más a una superclase. Es casi un tipo de objeto real, aunque no del todo (por ejemplo, la declaración de método de un protocolo no puede devolver a some).

Entonces, si fuera a usar somepara algo, lo más probable es que si que estuviera escribiendo un DSL o un marco / biblioteca para que otros lo usaran, y quisiera enmascarar los detalles de tipo subyacentes. Esto haría que su código sea más simple para que otros lo usen, y le permitiría cambiar los detalles de implementación sin romper su código.

Sin embargo, también puede usarlo en su propio código como una forma de proteger una región de su código de los detalles de implementación enterrados en otra región de su código.

mate
fuente
23

La somepalabra clave de Swift 5.1 ( propuesta de evolución rápida ) se usa 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 a qué protocolos se ajusta, 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 al que simplemente se ajusta Collection.


Tenga en cuenta también este posible error que puede enfrentar:

'algunos' tipos de devolución solo están disponibles en iOS 13.0.0 o posterior

Significa que se supone que debes usar la disponibilidad para evitar someen iOS 12 y antes:

@available(iOS 13.0, *)
func makeACollection() -> some Collection {
    ...
}
Coeur
fuente
1
Muchas gracias por esta respuesta enfocada y el problema del compilador en Xcode 11 beta
brainray
1
Se supone que debes usar la disponibilidad para evitar someen iOS 12 y versiones anteriores. Mientras lo hagas, deberías estar bien. El problema es solo que el compilador no te advierte que hagas esto.
mate
2
Cœur, tal como lo señala, la descripción concisa de Apple lo explica todo: las funciones ahora pueden ocultar su tipo de retorno concreto al declarar a qué protocolos se ajusta, en lugar de especificar el tipo de retorno exacto. Y luego el código que llama a la función puede usar la interfaz de protocolo. Aseado y algo más.
Fattie
Esto (ocultar el tipo de retorno concreto) ya es posible sin usar la palabra clave "some". No explica el efecto de agregar "algunos" en la firma del método.
Vince O'Sullivan
@ VinceO'Sullivan No es posible eliminar la somepalabra clave en este ejemplo de código dado en Swift 5.0 o Swift 4.2. El error será: "La 'Colección' del protocolo solo puede usarse como una restricción genérica porque tiene requisitos de tipo Self o asociados "
Cœur
2

'algunos' significa tipo opaco. En SwiftUI, View se declara como un protocolo

@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol 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 }
}

Cuando crea su vista como Struct, se ajusta al protocolo View y le dice que el cuerpo var devolverá algo que confirmará al protocolo View. Es como una abstracción de protocolo genérica en la que no tiene que definir el tipo concreto.

varunrathi28
fuente
2

Trataré de responder esto con un ejemplo práctico muy básico (¿de qué se trata este tipo de resultado opaco? )

Suponiendo que tiene un protocolo con un tipo asociado y dos estructuras que lo implementan:

protocol ProtocolWithAssociatedType {
    associatedtype SomeType
}

struct First: ProtocolWithAssociatedType {
    typealias SomeType = Int
}

struct Second: ProtocolWithAssociatedType {
    typealias SomeType = String
}

Antes de Swift 5.1, a continuación es ilegal debido a un ProtocolWithAssociatedType can only be used as a generic constrainterror:

func create() -> ProtocolWithAssociatedType {
    return First()
}

Pero en Swift 5.1 esto está bien ( someagregado):

func create() -> some ProtocolWithAssociatedType {
    return First()
}

Arriba está el uso práctico, ampliamente utilizado en SwiftUI para some View.

Pero hay una limitación importante: el tipo de retorno debe conocerse en el momento de la compilación, por lo que a continuación nuevamente no funcionará dando Function declares an opaque return type, but the return statements in its body do not have matching underlying typeserror:

func create() -> some ProtocolWithAssociatedType {
    if (1...2).randomElement() == 1 {
        return First()
    } else {
        return Second()
    }
}
tzaloga
fuente
0

Un caso de uso simple que me viene a la mente es escribir funciones genéricas para tipos numéricos.

/// Adds one to any decimal type
func addOne<Value: FloatingPoint>(_ x: Value) -> some FloatingPoint {
    x + 1
}

// Variables will be assigned 'some FloatingPoint' type
let double = addOne(Double.pi) // 4.141592653589793
let float = addOne(Float.pi) // 4.141593

// Still get all of the required attributes/functions by the FloatingPoint protocol
double.squareRoot() // 2.035090330572526
float.squareRoot() // 2.03509

// Be careful, however, not to combine 2 'some FloatingPoint' variables
double + double // OK 
//double + float // error
Artem Ilyumzhinov
fuente