El código genérico le permite escribir funciones y tipos flexibles y reutilizables que pueden funcionar con cualquier tipo, sujeto a los requisitos que defina. Puede escribir código que evite la duplicación y exprese su intención de manera clara y abstracta. - Documentos rápidos
Todos los que escribieron en Swift usaban genéricos. Array
, Dictionary
, Set
: las opciones más básicas para usar genéricos de la biblioteca estándar. ¿Cómo se representan dentro? Veamos cómo los ingenieros de Apple implementan esta característica fundamental del lenguaje.
Los parámetros genéricos pueden estar limitados por protocolos o no limitados, aunque, básicamente, los genéricos se usan junto con protocolos que describen qué se puede hacer exactamente con los parámetros del método o los campos de tipo.
Para implementar genéricos, Swift utiliza dos enfoques:
- Runtime-way: el código genérico es un contenedor (Boxing).
- Compiletime-way: el código genérico se convierte en un tipo específico de código para la optimización (Especialización).
Boxeo
Considere un método simple con un parámetro genérico de protocolo ilimitado:
func test<T>(value: T) -> T { let copy = value print(copy) return copy }
El compilador rápido crea un único bloque de código que se llamará para trabajar con cualquier <T>
. Es decir, independientemente de si escribimos test(value: 1)
o test(value: "Hello")
, se llamará al mismo código y adicionalmente la información sobre el tipo <T>
contiene toda la información necesaria se transferirá al método .
Poco se puede hacer con tales parámetros de protocolo ilimitados, pero para implementar este método, necesita saber cómo copiar un parámetro, necesita saber su tamaño para poder asignarle memoria en tiempo de ejecución, necesita saber cómo destruirlo cuando el parámetro abandona el campo. visibilidad. La Value Witness Table
( VWT
) se utiliza para almacenar esta información. VWT
se crea en la etapa de compilación para todos los tipos y el compilador garantiza que en el tiempo de ejecución habrá tal diseño del objeto. Permítame recordarle que las estructuras en Swift se pasan por valor y las clases por referencia, por lo que se harán diferentes cosas para let copy = value
con T == MyClass
y T == MyStruct
.
Es decir, llamar al método de test
al pasar la estructura declarada allí eventualmente se verá así:
Las cosas se vuelven un poco más complicadas cuando MyStruct
sí una estructura genérica y toma la forma MyStruct<T>
. Dependiendo de <T>
dentro de MyStruct
, los metadatos y VWT
serán diferentes para los tipos MyStruct<Int>
y MyStruct<Bool>
. Estos son dos tipos diferentes en tiempo de ejecución. Pero crear metadatos para cada combinación posible de MyStruct
y T
extremadamente ineficiente, por lo que Swift va en sentido contrario, y para tales casos construye metadatos en tiempo de ejecución sobre la marcha. El compilador crea un patrón de metadatos para la estructura genérica, que se puede combinar con un tipo específico y, como resultado, recibir información de tipo completa en tiempo de ejecución con el VWT
correcto.
Cuando combinamos información, obtenemos metadatos con los que podemos trabajar (copiar, mover, destruir).
Todavía es un poco más complicado cuando se agregan restricciones de protocolo a los genéricos. Por ejemplo, restringimos <T>
protocolo Equatable
. Sea un método muy simple que compare los dos argumentos pasados. El resultado es solo un contenedor sobre el método de comparación.
func isEquals<T: Equatable>(first: T, second: T) -> Bool { return first == second }
Para que el programa funcione correctamente, debe tener un puntero al método de comparación static func ==(lhs:T, rhs:T)
. ¿Cómo conseguirlo? Obviamente, la transmisión VWT
no VWT
suficiente, no contiene esta información. Para resolver este problema, hay una Protocol Witness Table
o PWT
. Esta VWT
es similar a VWT
y se crea en la etapa de compilación de protocolos y describe estos protocolos.
isEquals(first: 1, second: 2)
- Dos argumentos pasaron
- Pase metadatos para
Int
para que pueda copiar / mover / destruir objetos - Pasamos la información que
Int
implementa Equatable
.
Si la restricción requería la implementación de otro protocolo, por ejemplo, T: Equatable & MyProtocol
, entonces se MyProtocol
información sobre MyProtocol
con el siguiente parámetro:
isEquals(..., intIsEquatable: Equatable.witnessTable, intIsMyProtocol: MyProtocol.witnessTable)
El uso de envoltorios para implementar genéricos le permite implementar de manera flexible todas las características necesarias, pero tiene una sobrecarga que puede optimizarse.
Especialización genérica
Para eliminar la necesidad innecesaria de obtener información durante la ejecución del programa, se utilizó el llamado enfoque de especialización genérico. Le permite reemplazar un contenedor genérico con un tipo específico con una implementación específica. Por ejemplo, para dos llamadas a isEquals(first: 1, second: 2)
e isEquals(first: "Hello", second: "world")
, además de la implementación principal "wrapper", dos versiones adicionales completamente diferentes del método para Int
y para la String
.
Código fuente
Primero, cree un archivo generic.swift y escriba una pequeña función genérica que consideraremos.
func isEquals<T: Equatable>(first: T, second: T) -> Bool { return first == second } isEquals(first: 10, second: 11)
Ahora necesita comprender lo que eventualmente se convierte en un compilador.
Esto se puede ver claramente compilando nuestro archivo .swift en Swift Intermediate Language o SIL
.
Un poco sobre SIL y el proceso de compilación
SIL
es el resultado de una de varias etapas de compilación rápida.
El código fuente .swift se pasa a Lexer, que crea un árbol de sintaxis abstracta ( AST
) del lenguaje, sobre la base de qué tipo de verificación y análisis semántico se lleva a cabo. SilGen convierte AST
a SIL
, llamado raw SIL
, sobre la base de qué código se optimiza y canonical SIL
obtiene un canonical SIL
optimizado, que se pasa a IRGen
para su conversión a IR
, un formato especial que LLVM
entiende, que se convertirá en , .
` .o , .
, .
SIL`.
Y de nuevo a los genéricos.
Cree un archivo SIL
partir de nuestro código fuente.
swiftc generic.swift -O -emit-sil -o generic-sil.s
Obtenemos un nuevo archivo con la extensión *.s
. Mirando hacia adentro, veremos un código mucho menos legible que el original, pero aún relativamente claro.
Busque la línea con el comentario // isEquals<A>(first:second:)
. Este es el comienzo de la descripción de nuestro método. Termina con un comentario // end sil function '$s4main8isEquals5first6secondSbx_xtSQRzlF'
. Tu nombre puede ser ligeramente diferente. Analicemos un poco la descripción del método.
%0
y %1
en la línea 21 son los parámetros first
y second
, respectivamente- En la línea 24 obtenemos información de tipo y la pasamos a
%4
- En la línea 25 obtenemos un puntero a un método de comparación a partir de información de tipo
- en la línea 26 Llamamos al método por puntero, pasándole tanto parámetros como información de tipo
- En la línea 27 damos el resultado.
Como resultado, vemos: para realizar las acciones necesarias en la implementación del método genérico, necesitamos obtener información de la descripción del tipo <T>
durante la ejecución del programa.
Procedemos directamente a la especialización.
En el archivo SIL
compilado, inmediatamente después de la declaración del método general isEquals
, isEquals
la declaración del especialista para el tipo Int
.
En la línea 39, en lugar de obtener el método en tiempo de ejecución a partir de la información de tipo, "cmp_eq_Int64"
llama inmediatamente al método para comparar enteros "cmp_eq_Int64"
.
Para que el método se "especialice", la optimización debe estar habilitada . También necesitas saber que
El optimizador solo puede realizar la especialización si la definición de la declaración genérica es visible en el Módulo actual ( Fuente )
Es decir, el método no puede especializarse entre diferentes módulos Swift (por ejemplo, el método genérico de la biblioteca Cocoapods). Una excepción es la biblioteca estándar de Swift, en la que tipos básicos como Array
, Set
y Dictionary
. Todos los genéricos de la biblioteca base se especializan en tipos específicos.
Nota: Los atributos @inlinable
y @usableFromInline
se implementaron en Swift 4.2, lo que permite al optimizador ver los cuerpos de los métodos de otros módulos y parece que hay una oportunidad para especializarlos, pero este comportamiento no fue probado por mí ( Fuente )
Referencias
- Descripción de genéricos.
- Optimización en Swift
- Presentación más detallada y detallada sobre el tema.
- Artículo en inglés