Introducción El La palabra clave fue introducida en Swift 5.1, mientras que palabra clave fue introducida en Swift 5.6. Ahora, estas palabras clave se pueden utilizar en los parámetros de la función y antes de la definición de un tipo, de la siguiente manera: some any protocol P {} struct S: P {} // 'any P' is an explicit existential type. let p: any P = S() // 'some P' is an opaque type. let p: some P = S() func f(_ p: any P) {} func f(_ p: some P) {} In Swift 5, the Las palabras clave se pueden utilizar para denotar explícitamente un tipo existencial. A partir de Swift 6, se requiere que los tipos existenciales se escriban explícitamente con el keyword. any any An opaque type helps describe the expected return type without defining a specific concrete type. This way, the compiler can get access to the actual type information and can potentially perform optimizations in this context. Un tipo existencial puede almacenar cualquier valor que se ajuste al protocolo, y el tipo del valor puede cambiar dinámicamente, lo que requiere una asignación de memoria dinámica. Comprender algunas palabras clave keyword represents an opaque type. An opaque result type is an implicit generic placeholder satisfied by the implementation, so you can think of this: some protocol P {} struct S1 : P {} func f() -> some P { return S1() } La clave aquí es que una función que produce un tipo devuelve específicamente un valor de un tipo concreto singular que se adhiere a Si la función intenta devolver varios tipos de conformidad, resultará en un error de compilador, ya que el poseedor de lugar genérico implícito no puede ser satisfecho por varios tipos. P P struct F1: P {} struct F2: P {} // error: Function declares an opaque return type, but the return // statements in its body do not have matching underlying types. func f(_ value: Bool) -> some P { if value { return F1() } else { return F2() } } Let’s consider the benefits that opaque types offer over protocol return types. Opaque result types can be used with PATs. The main limitation of using protocols is that protocols with associated types cannot be used as actual types. This means that the following code doesn’t compile: func collection() -> Collection { return ["1", "2", "3"] } As for opaque types, they are merely generic placeholders that can be used in such scenarios: // protocol Collection<Element> : Sequence func collection() -> some Collection { return ["1", "2", "3"] } Opaque result types have identity. Because opaque types guarantee that only one type will be returned, the compiler knows that a function must return the same type on several calls: func method() -> some Equatable { return "method" } let x = method() let y = method() print(x == y) // true Opaque result types compose with generic placeholders. Contrary to conventional protocol-typed values, opaque result types integrate effectively with standard generic placeholders. For instance: protocol P { var message: String { get } } struct M: P { var message: String } func makeM() -> some P { return M(message: "message") } func bar<T: P, U: P>(_ p1: T, _ p2: U) -> Bool { return p1.message == p2.message } let m1 = makeM() let m2 = makeM() print(bar(m1, m2)) However, it doesn’t work if make returns different types based on protocol . M() P protocol P { var message: String { get } } struct M: P { var message: String } struct T: P { var message: String } // error: function declares an opaque return type 'some P', but the return statements in its body do not have matching underlying types func makeM() -> some P { if .random() { return M(message: "M message") } else { return T(message: "T message") } } Comprender cualquier palabra clave Veamos el siguiente ejemplo: Supongamos que tenemos un Protocolo y dos implementaciones concretas de este Protocolo. Drawable protocol Drawable { func draw() } struct Line: Drawable { let x1: Int let y1: Int let x2: Int let y2: Int func draw() { print("Draw Line") } } struct Point: Drawable { let x: Int let y: Int func draw() { print("Point") } } Tenemos dos objetos estructurales, y Crear una variable y almacenar una object: Line Point Drawable var p1: any Drawable = Line(x1: 0, y1: 0, x2: 5, y2: 5) // 'any Drawable' is an explicit existential type p1.draw() // print "Draw Line" p1 = Point(x: 0, y: 0) p1.draw() // print "Point" Podemos cambiar entre diferentes implementaciones durante el tiempo de ejecución. Consideremos otro ejemplo: let array: [any Drawable] = [ Line(x1: 0, y1: 0, x2: 5, y2: 5), Line(x1: 0, y1: 0, x2: 5, y2: 5), Point(x: 0, y: 0) ] Como sabemos, lograr el acceso aleatorio a cualquier elemento dentro de una matriz en tiempo constante es posible debido a que cada elemento tiene el mismo tamaño de memoria. En nuestro ejemplo, donde almacenamos elementos de tamaños diferentes, puede preguntarse cómo podemos recuperar un elemento de la matriz en tal escenario. print(MemoryLayout<Line>.size) // 32 print(MemoryLayout<Point>.size) // 16 Es posible porque el means indicates that we work with existential containers. The existential container encapsulates five machine words, allocating three for storing an object or a pointer to the object, one for a pointer to the virtual table, and another for a pointer to the witness table. any El contenedor existencial toma 5 palabras de máquina (en un sistema x64-bit, 5 * 8 = 40): Value buffer is a space for the instance VWT is a pointer to Value Witness Table PWT is a pointer to the Protocol Witness Table Cada vez que se utiliza un valor del protocolo en el código, el compilador genera una caja que llamamos un contenedor existencial. Una caja para un valor. Puesto que pueden almacenar cualquier valor cuyo tipo se ajusta al protocolo, y el tipo del valor almacenado puede cambiar dinámicamente, el contenedor existencial requiere asignación de memoria dinámica si el valor no es lo suficientemente pequeño como para encajar en un buffer de 3 palabras de máquina. Además de la asignación de pilas y el conteo de referencia, cada uso de un contenedor existencial implica indirección de indicadores y envío dinámico, que no se puede optimizar. Se utilizan tres palabras para enmarcar un valor si coincide con 3 palabras de máquina, o, si el valor es más de 3 palabras de máquina, se crea una caja gestionada por ARC. Se copia el valor en la caja y se copia un apuntal hacia la caja gestionada por ARC en la primera palabra del contenedor. Las dos palabras restantes no se utilizan. Las siguientes palabras se utilizan para apuntar a la Tabla de testigos de valor (VWT) y la Tabla de testigos de protocolo (PWT), respectivamente. La Tabla de Testigos de Protocolo es una matriz que contiene una entrada para cada protocolo estatisticamente asociado con el tipo existencial. , habrá dos entradas, una para cada protocolo. Mientras tengamos la Tabla de Testigos del Protocolo (PWT) que describe la adherencia del tipo al protocolo, podemos crear un contenedor existencial, pasarlo y usarlo para el envío dinámico. P & Q Por lo tanto, tenemos un contenedor existencial, y tenemos una tabla de testigos de protocolo. Sin embargo, no sabemos dónde debe almacenarse este valor en el contenedor existencial – si se almacena directamente en el contenedor o si solo tenemos una referencia al valor en el montón. Para responder a estas preguntas, nos dirigimos a una tabla auxiliar llamada Tabla de testigos de valor (VWT). , , de , de Cada tipo tiene una tabla, y asume la responsabilidad de crear una instancia del tipo, decidir dónde debe almacenarse el valor: en la pila o en la pila, etc. allocate copy destruct deallocate Ahora tenemos un contenedor de tamaño fijo, que resuelve el problema de pasar parámetros de tamaños heterogéneos, y una manera de mover este contenedor. Bueno, al menos, queremos poder invocar los miembros necesarios del valor almacenado en el contenedor, tal y como se especifica en el protocolo (inicializadores, propiedades, ya sean almacenadas o calculadas, funciones y suscripciones). Sin embargo, cada tipo correspondiente puede implementar estos miembros de manera diferente. Por ejemplo, algunos pueden satisfacer el requisito del método definiéndolo directamente, mientras que otro, digamos una clase, puede satisfacerlo heredando el método de una superclase. Algunos pueden implementar una propiedad como una propiedad almacenada, otros como una computada, y algunos retroactivamente (a través de una extensión). Gestionar toda esta diversidad es el propósito principal de la Tabla de Testigos de Protocolo (PWT). Hay exactamente una tabla de este tipo para cada acuerdo de protocolo. Si bien las funciones pueden estar ubicadas en diferentes lugares, la ubicación del PWT es única para el protocolo al que corresponde. Como resultado, el llamador puede inspeccionar el PWT del contenedor, usarlo para localizar la implementación de los métodos del protocolo, y invocarlos. Es importante saber que los existenciales son relativamente caros de usar porque el compilador y el tiempo de ejecución no pueden predeterminar cuánta memoria debe asignarse al objeto concreto que llenará el existencial. Es importante saber que los existenciales son relativamente caros de usar porque el compilador y el tiempo de ejecución no pueden predeterminar cuánta memoria debe asignarse al objeto concreto que llenará el existencial. Resumen We considered the differences between y Por un lado, mejoró significativamente la sintaxis y la legibilidad de nuestro código genérico; por otro lado, introdujo nuevas vías para que podamos elaborar código genérico de una manera más eficiente. some any Gracias por la lectura.