swift - protocolos - ¿Cuál es la diferencia en la práctica entre los parámetros de función genéricos y de protocolo?
que es un protocolo en swift (2)
Dado un protocolo sin ningún tipo asociado:
protocol SomeProtocol
{
var someProperty: Int { get }
}
¿Cuál es la diferencia entre estas dos funciones, en la práctica (lo que significa que no "una es genérica y la otra no")? ¿Generan código diferente, tienen características de tiempo de ejecución diferentes? ¿Estas diferencias cambian cuando el protocolo o las funciones se vuelven no triviales? (ya que un compilador probablemente podría incluir algo como esto)
func generic<T: SomeProtocol>(some: T) -> Int
{
return some.someProperty
}
func nonGeneric(some: SomeProtocol) -> Int
{
return some.someProperty
}
Principalmente estoy preguntando sobre las diferencias en lo que hace el compilador, entiendo las implicaciones a nivel de lenguaje de ambos.
Básicamente, ¿no
nonGeneric
implica un tamaño de código constante pero un despacho dinámico más lento, frente a un
generic
usa un tamaño de código creciente por tipo pasado, pero con un despacho estático rápido?
Si su método
generic
tuviera más de un parámetro que involucrara a
T
, habría una diferencia.
func generic<T: SomeProtocol>(some: T, someOther: T) -> Int
{
return some.someProperty
}
En el método anterior,
some
y
someOther
deben ser del mismo tipo.
Pueden ser de cualquier tipo que se ajuste a
SomeProtocol
, pero tienen que ser del
mismo
tipo.
Sin embargo, sin genéricos:
func nonGeneric(some: SomeProtocol, someOther: SomeProtocol) -> Int
{
return some.someProperty
}
some
y
someOther
pueden ser de
diferentes
tipos, siempre que se ajusten a
SomeProtocol
.
(Me doy cuenta de que OP pregunta menos sobre las implicaciones del lenguaje y más sobre lo que hace el compilador, pero creo que también vale la pena enumerar las diferencias generales entre los parámetros de función genéricos y de protocolo)
1. Un marcador de posición genérico limitado por un protocolo debe estar satisfecho con un tipo concreto
Esto es una consecuencia de los
protocolos que no se ajustan a sí mismos
, por lo tanto, no se puede llamar
generic(some:)
con un argumento de tipo
SomeProtocol
.
struct Foo : SomeProtocol {
var someProperty: Int
}
// of course the solution here is to remove the redundant ''SomeProtocol'' type annotation
// and let foo be of type Foo, but this problem is applicable anywhere an
// ''anything that conforms to SomeProtocol'' typed variable is required.
let foo : SomeProtocol = Foo(someProperty: 42)
generic(some: something) // compiler error: cannot invoke ''generic'' with an argument list
// of type ''(some: SomeProtocol)''
Esto se debe a que la función genérica espera un argumento de algún tipo
T
que se ajuste a
SomeProtocol
, pero
SomeProtocol
no
es un tipo que se ajuste a
SomeProtocol
.
Sin embargo, una función no genérica, con un tipo de parámetro de
SomeProtocol
, aceptará
foo
como argumento:
nonGeneric(some: foo) // compiles fine
Esto se debe a que acepta "cualquier cosa que pueda escribirse como
SomeProtocol
", en lugar de "un tipo específico que se ajuste a
SomeProtocol
".
2. Especialización
Como se cubre en esta fantástica charla de WWDC , se usa un ''contenedor existencial'' para representar un valor de tipo de protocolo.
Este contenedor consta de:
-
Un búfer de valores para almacenar el valor en sí, que tiene 3 palabras de longitud. Los valores superiores a este se asignarán en el montón, y una referencia al valor se almacenará en el búfer de valores (como referencia tiene un tamaño de solo 1 palabra).
-
Un puntero a los metadatos del tipo. En los metadatos del tipo se incluye un puntero a su tabla de testigos de valor, que administra la vida útil del valor en el contenedor existencial.
-
Uno o (en el caso de la composición del protocolo ) múltiples punteros a las tablas de testigos de protocolo para el tipo dado. Estas tablas realizan un seguimiento de la implementación del tipo de los requisitos de protocolo disponibles para llamar en la instancia de tipo de protocolo dada.
De forma predeterminada, se utiliza una estructura similar para pasar un valor a un argumento escrito con marcador de posición genérico.
-
El argumento se almacena en un búfer de valor de 3 palabras (que puede asignarse en un montón), que luego se pasa al parámetro.
-
Para cada marcador de posición genérico, la función toma un parámetro de puntero de metadatos. El metatipo del tipo que se usa para satisfacer el marcador de posición se pasa a este parámetro cuando se llama.
-
Para cada restricción de protocolo en un marcador de posición dado, la función toma un parámetro de puntero de tabla testigo de protocolo.
Sin embargo, en las compilaciones optimizadas, Swift puede especializar las implementaciones de funciones genéricas, lo que permite al compilador generar una nueva función para cada tipo de marcador de posición genérico con el que se aplica. Esto permite que los argumentos siempre se pasen simplemente por valor, a costa de aumentar el tamaño del código. Sin embargo, como se dice en la charla, las optimizaciones agresivas del compilador, particularmente la alineación, pueden contrarrestar esta hinchazón.
3. Despacho de requisitos de protocolo
Debido al hecho de que las funciones genéricas pueden especializarse, las llamadas a métodos sobre argumentos genéricos pasados pueden enviarse estáticamente (aunque obviamente no para los tipos que usan polimorfismo dinámico, como las clases no finales).
Sin embargo, las funciones de tipo protocolo generalmente no pueden beneficiarse de esto, ya que no se benefician de la especialización. Por lo tanto, las llamadas a métodos en un argumento de tipo protocolo se enviarán dinámicamente a través de la tabla de testigos de protocolo para ese argumento dado, que es más costoso.
Aunque dicho esto, las funciones simples de tipo protocolo
pueden
beneficiarse de la inserción.
En tales casos, el compilador puede eliminar la sobrecarga del búfer de valor y las tablas de protocolo y testigo de valor (esto se puede ver examinando el SIL emitido en una compilación -O), lo que le permite enviar métodos estáticos de la misma manera que funciones genéricas
Sin embargo, a diferencia de la especialización genérica, esta optimización no está garantizada para una función determinada (a menos que
aplique el
@inline(__always)
, pero generalmente es mejor dejar que el compilador decida esto).
Por lo tanto, en general, las funciones genéricas se prefieren a las funciones de tipo protocolo en términos de rendimiento, ya que pueden lograr el envío estático de métodos sin tener que estar en línea.
4. Resolución de sobrecarga
Al realizar la resolución de sobrecarga, el compilador favorecerá la función de tipo protocolo sobre la genérica.
struct Foo : SomeProtocol {
var someProperty: Int
}
func bar<T : SomeProtocol>(_ some: T) {
print("generic")
}
func bar(_ some: SomeProtocol) {
print("protocol-typed")
}
bar(Foo(someProperty: 5)) // protocol-typed
Esto se debe a que Swift favorece un parámetro tipado explícitamente sobre uno genérico (consulte estas preguntas y respuestas ).
5. Los marcadores de posición genéricos imponen el mismo tipo
Como ya se dijo, el uso de un marcador de posición genérico le permite exigir que se use el mismo tipo para todos los parámetros / devoluciones que se escriben con ese marcador de posición en particular.
La función:
func generic<T : SomeProtocol>(a: T, b: T) -> T {
return a.someProperty < b.someProperty ? b : a
}
toma dos argumentos y tiene un retorno del
mismo
tipo concreto, donde ese tipo se ajusta a
SomeProtocol
.
Sin embargo, la función:
func nongeneric(a: SomeProtocol, b: SomeProtocol) -> SomeProtocol {
return a.someProperty < b.someProperty ? b : a
}
no lleva más promesas que los argumentos y el retorno debe cumplir con
SomeProtocol
.
Los tipos concretos reales que se pasan y devuelven no necesariamente tienen que ser los mismos.