¿Por qué no compila este código Swift?
protocol P { }
struct S: P { }
let arr:[P] = [ S() ]
extension Array where Element : P {
func test<T>() -> [T] {
return []
}
}
let result : [S] = arr.test()
El compilador dice: "El tipo P
no se ajusta al protocolo P
" (o, en versiones posteriores de Swift, "No se admite el uso de 'P' como tipo concreto conforme al protocolo 'P'").
Por qué no? Esto se siente como un agujero en el idioma, de alguna manera. Me doy cuenta de que el problema surge de declarar la matriz arr
como una matriz de un tipo de protocolo , pero ¿es eso algo irracional? ¿Pensé que los protocolos estaban allí exactamente para ayudar a suministrar estructuras con algo así como una jerarquía de tipos?
let arr
línea, el compilador infiere el tipo[S]
y el código se compila. Parece que un tipo de protocolo no se puede usar de la misma manera que una relación clase - superclase.protocol P : Q { }
P no se ajusta a Q.Respuestas:
EDITAR: Dieciocho meses más de trabajo con Swift, otra versión importante (que proporciona un nuevo diagnóstico), y un comentario de @AyBayBay me dan ganas de volver a escribir esta respuesta. El nuevo diagnóstico es:
Eso realmente hace que todo esto sea mucho más claro. Esta extensión:
no se aplica cuando
Element == P
yaP
no se considera una conformidad concreta deP
. (La solución "ponerlo en una caja" a continuación sigue siendo la solución más general).Vieja respuesta:
Es otro caso más de metatipos. Swift realmente quiere que llegue a un tipo concreto para la mayoría de las cosas no triviales.(No creo que eso sea realmente cierto; puedes crear absolutamente algo de tamaño[P]
no es un tipo concreto (no puede asignar un bloque de memoria de tamaño conocidoP
).P
porque se hace por vía indirecta ). No creo que haya ninguna evidencia de que este sea un caso de "no debería" funcionar. Esto se parece mucho a uno de sus casos de "todavía no funciona". (Desafortunadamente, es casi imposible hacer que Apple confirme la diferencia entre esos casos). El hecho de queArray<P>
puede ser un tipo variable (dondeArray
no puede) indica que ya han hecho algo de trabajo en esta dirección, pero los metatipos Swift tienen muchos bordes afilados y casos sin implementar. No creo que vaya a obtener una mejor respuesta "por qué" que eso. "Porque el compilador no lo permite". (Insatisfactorio, lo sé. Toda mi vida de Swift ...)La solución es casi siempre poner las cosas en una caja. Construimos un borrador de texto.
Cuando Swift te permite hacer esto directamente (lo que espero eventualmente), es probable que solo sea creando este cuadro automáticamente. Las enumeraciones recursivas tenían exactamente esta historia. Tenías que encajonarlos y fue increíblemente molesto y restrictivo, y finalmente el compilador agregó
indirect
que hiciera lo mismo más automáticamente.fuente
==
en mi ejemplo de matriz, obtenemos un error, el requisito del mismo tipo hace que el parámetro genérico 'Elemento' no sea genérico. "¿Por qué el uso de Tomohiro de no==
genera el mismo error?¿Por qué los protocolos no se ajustan a sí mismos?
Permitir que los protocolos se ajusten a sí mismos en el caso general es poco sólido. El problema radica en los requisitos del protocolo estático.
Éstos incluyen:
static
métodos y propiedadesPodemos acceder a estos requisitos en un marcador de posición genérico
T
dondeT : P
, sin embargo, no podemos acceder a ellos en el tipo de protocolo en sí, ya que no hay ningún tipo de conformación concreta para reenviar. Por lo tanto no podemos permitirT
serP
.Considere lo que sucedería en el siguiente ejemplo si permitiéramos que la
Array
extensión sea aplicable a[P]
:No podemos recurrir
appendNew()
a a[P]
, porqueP
(elElement
) no es un tipo concreto y, por lo tanto, no se puede instanciar. Se debe invocar en una matriz con elementos de tipo concreto, donde ese tipo se ajustaP
.Es una historia similar con el método estático y los requisitos de propiedad:
No podemos hablar en términos de
SomeGeneric<P>
. Necesitamos implementaciones concretas de los requisitos del protocolo estático (observe cómo no hay implementaciones defoo()
obar
definidas en el ejemplo anterior). Si bien podemos definir implementaciones de estos requisitos en unaP
extensión, estos se definen solo para los tipos concretos que se ajustan a ellosP
; aún no puedeP
invocarlos.Debido a esto, Swift simplemente nos impide usar un protocolo como un tipo que se ajusta a sí mismo, porque cuando ese protocolo tiene requisitos estáticos, no los tiene.
Los requisitos del protocolo de instancia no son problemáticos, ya que debe llamarlos en una instancia real que se ajuste al protocolo (y, por lo tanto, debe haber implementado los requisitos). Entonces, cuando se llama a un requisito en una instancia escrita como
P
, simplemente podemos reenviar esa llamada a la implementación de ese requisito por parte del tipo concreto subyacente.Sin embargo, hacer excepciones especiales para la regla en este caso podría conducir a inconsistencias sorprendentes en la forma en que los protocolos son tratados por código genérico. Aunque dicho esto, la situación no es muy diferente de los
associatedtype
requisitos, que (actualmente) le impiden usar un protocolo como tipo. Tener una restricción que le impide usar un protocolo como un tipo que se ajusta a sí mismo cuando tiene requisitos estáticos podría ser una opción para una futura versión del idiomaEditar: y como se explora a continuación, esto se parece a lo que el equipo Swift está buscando.
@objc
protocolosY, de hecho, así es exactamente como el lenguaje trata los
@objc
protocolos. Cuando no tienen requisitos estáticos, se conforman a sí mismos.Lo siguiente compila muy bien:
baz
requiere que seT
ajuste aP
; pero podemos sustituir enP
paraT
, porqueP
no tiene requisitos estáticos. Si agregamos un requisito estático aP
, el ejemplo ya no se compila:Entonces, una solución a este problema es hacer su protocolo
@objc
. Por supuesto, esta no es una solución ideal en muchos casos, ya que obliga a sus tipos conformes a ser clases, además de requerir el tiempo de ejecución Obj-C, por lo que no lo hace viable en plataformas que no son de Apple como Linux.Pero sospecho que esta limitación es (una de) las razones principales por las que el lenguaje ya implementa 'protocolo sin requisitos estáticos se ajusta a sí mismo' para
@objc
protocolos. El compilador puede simplificar significativamente el código genérico escrito alrededor de ellos.¿Por qué? Debido a que
@objc
los valores de protocolo-tecleado son referencias efectivamente acaba de clase cuyos requisitos se envían utilizandoobjc_msgSend
. Por otro lado, los@objc
valores no tipificados por protocolo son más complicados, ya que transportan tanto tablas de valores como de testigo para administrar la memoria de su valor empaquetado (potencialmente almacenado indirectamente) y determinar qué implementaciones llamar para las diferentes requisitos, respectivamente.Debido a esta representación simplificada para
@objc
protocolos, un valor de dicho tipo de protocoloP
puede compartir la misma representación de memoria que un 'valor genérico' de tipo algún marcador de posición genéricoT : P
, presumiblemente haciendo que sea fácil para el equipo de Swift permitir la autosuficiencia.@objc
Sin embargo, lo mismo no es cierto para los que no son protocolos, ya que dichos valores genéricos no tienen actualmente tablas de valores o testigos de protocolo.Sin embargo, esta característica es intencional y es de esperar que sea implementada en no
@objc
protocolos, como lo confirmó Slava Pestov, miembro del equipo de Swift, en los comentarios del SR-55 en respuesta a su consulta al respecto (provocada por esta pregunta ):Espero que sea algo que el lenguaje algún día también admitirá para los no
@objc
protocolos.Pero, ¿qué soluciones actuales hay para los no
@objc
protocolos?Implementación de extensiones con restricciones de protocolo
En Swift 3.1, si desea una extensión con la restricción de que un marcador de posición genérico dado o un tipo asociado debe ser un tipo de protocolo dado (no solo un tipo concreto que se ajuste a ese protocolo), simplemente puede definir esto con una
==
restricción.Por ejemplo, podríamos escribir su extensión de matriz como:
Por supuesto, esto ahora nos impide llamarlo en una matriz con elementos de tipo concreto que se ajustan a
P
. Podríamos resolver esto simplemente definiendo una extensión adicional para cuándoElement : P
y simplemente avanzar a la== P
extensión:Sin embargo, vale la pena señalar que esto realizará una conversión O (n) de la matriz a a
[P]
, ya que cada elemento tendrá que estar en un contenedor existencial. Si el rendimiento es un problema, simplemente puede resolverlo volviendo a implementar el método de extensión. Esta no es una solución completamente satisfactoria; es de esperar que una versión futura del lenguaje incluya una forma de expresar una restricción de 'tipo de protocolo o conforme al tipo de protocolo'.Antes de Swift 3.1, la forma más general de lograr esto, como lo muestra Rob en su respuesta , es simplemente construir un tipo de contenedor para a
[P]
, en el que luego puede definir sus métodos de extensión.Pasar una instancia de tipo de protocolo a un marcador de posición genérico restringido
Considere la siguiente situación (artificial, pero no infrecuente):
No podemos pasar
p
atakesConcreteP(_:)
, ya que actualmente no podemos sustituirP
a un marcador de posición genéricoT : P
. Echemos un vistazo a un par de formas en que podemos resolver este problema.1. Aperturas existenciales
En lugar de intentar sustituir
P
porT : P
, qué pasaría si pudiéramos cavar en el tipo de hormigón subyacente de que elP
valor con tipo era envolver y sustituto que en lugar? Desafortunadamente, esto requiere una función de lenguaje llamada existenciales de apertura , que actualmente no está disponible directamente para los usuarios.Sin embargo, Swift hace implícitamente existenciales abiertas (valores de protocolo-mecanografiado) cuando se accede a los miembros en ellos (es decir, se excava a cabo el tipo de tiempo de ejecución y hace que sea accesible en forma de un marcador de posición genérico). Podemos explotar este hecho en una extensión de protocolo en
P
:Tenga en cuenta el
Self
marcador de posición genérico implícito que toma el método de extensión, que se utiliza para escribir elself
parámetro implícito ; esto sucede detrás de escena con todos los miembros de extensión de protocolo. Al llamar a un método de este tipo en un valor de protocolo escritoP
, Swift desentierra el tipo concreto subyacente y lo utiliza para satisfacer elSelf
marcador de posición genérico. Es por esto que estamos en condiciones de llamartakesConcreteP(_:)
aself
- estamos satisfaciendoT
conSelf
.Esto significa que ahora podemos decir:
Y
takesConcreteP(_:)
se llama con su marcador de posición genéricoT
satisfecho por el tipo de hormigón subyacente (en este casoS
). Tenga en cuenta que esto no es "protocolos que se ajustan a sí mismos", ya que estamos sustituyendo un tipo concreto en lugar deP
: intente agregar un requisito estático al protocolo y ver qué sucede cuando lo llama desde adentrotakesConcreteP(_:)
.Si Swift continúa impidiendo que los protocolos se ajusten a sí mismos, la siguiente mejor alternativa sería abrir implícitamente los existenciales al intentar pasarlos como argumentos a parámetros de tipo genérico, efectivamente haciendo exactamente lo que hizo nuestro trampolín de extensión de protocolo, solo sin la placa repetitiva.
Sin embargo, tenga en cuenta que abrir existenciales no es una solución general al problema de los protocolos que no se ajustan a sí mismos. No trata con colecciones heterogéneas de valores tipificados por protocolo, que pueden tener diferentes tipos concretos subyacentes. Por ejemplo, considere:
Por las mismas razones, una función con múltiples
T
parámetros también sería problemática, ya que los parámetros deben tomar argumentos del mismo tipo; sin embargo, si tenemos dosP
valores, no hay forma de garantizar en el momento de la compilación que ambos tengan el mismo concreto subyacente. tipo.Para resolver este problema, podemos usar un borrador de tipo.
2. Construye un borrador de tipo
Como dice Rob , un borrador tipo , es la solución más general al problema de los protocolos que no se ajustan a sí mismos. Nos permiten ajustar una instancia de tipo de protocolo en un tipo concreto que se ajusta a ese protocolo, reenviando los requisitos de la instancia a la instancia subyacente.
Entonces, construyamos un cuadro de borrado de tipo que reenvíe
P
los requisitos de instancia a una instancia arbitraria subyacente que se ajuste aP
:Ahora podemos hablar en términos de en
AnyP
lugar deP
:Ahora, considere por un momento por qué tuvimos que construir esa caja. Como discutimos anteriormente, Swift necesita un tipo concreto para los casos en que el protocolo tiene requisitos estáticos. Considere si
P
tuviera un requisito estático: habríamos tenido que implementarlo enAnyP
. Pero, ¿cómo debería haberse implementado? Estamos lidiando con instancias arbitrarias que se conformanP
aquí: no sabemos cómo sus tipos concretos subyacentes implementan los requisitos estáticos, por lo tanto, no podemos expresar esto de manera significativaAnyP
.Por lo tanto, la solución en este caso solo es realmente útil en el caso de los requisitos de protocolo de instancia . En el caso general, todavía no podemos tratar
P
como un tipo concreto que se ajuste aP
.fuente
P
) está bien porque simplemente podemos reenviar llamadas a los requisitos de la instancia a la instancia subyacente. Sin embargo, para un tipo de protocolo en sí mismo (es decirP.Protocol
, literalmente solo el tipo que describe un protocolo), no hay un adoptante, por lo tanto, no hay nada a lo que recurrir los requisitos estáticos, por lo que en el ejemplo anterior no podemos tenerSomeGeneric<P>
(es diferente para unP.Type
(metatipo existencial), que describe un metatipo concreto de algo que se ajusta aP
, pero esa es otra historia)P
) y los metatipos existenciales (es decir, losP.Type
metatipos). El problema es que para los genéricos, en realidad no estamos comparando lo mismo. CuandoT
es asíP
, no hay ningún tipo de hormigón subyacente (meta) para reenviar los requisitos estáticos a (T
es unP.Protocol
, no unP.Type
) ...Si extiende el
CollectionType
protocolo en lugar de unaArray
restricción por protocolo como un tipo concreto, puede volver a escribir el código anterior de la siguiente manera.fuente
== P
vs: P
. Con == el ejemplo original también funciona. Y un problema potencial (dependiendo del contexto) con == es que excluye sub-protocolos: si creo unaprotocol SubP: P
, y luego definoarr
como[SubP]
entoncesarr.test()
ya no funcionarán (error: SUBP y P deben ser equivalentes).