¿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 Pno 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 arrcomo 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 arrlí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 == PyaPno 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).Pporque 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 (dondeArrayno 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ó
indirectque 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:
staticmétodos y propiedadesPodemos acceder a estos requisitos en un marcador de posición genérico
TdondeT : 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 permitirTserP.Considere lo que sucedería en el siguiente ejemplo si permitiéramos que la
Arrayextensió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()obardefinidas en el ejemplo anterior). Si bien podemos definir implementaciones de estos requisitos en unaPextensión, estos se definen solo para los tipos concretos que se ajustan a ellosP; aún no puedePinvocarlos.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
associatedtyperequisitos, 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.
@objcprotocolosY, de hecho, así es exactamente como el lenguaje trata los
@objcprotocolos. Cuando no tienen requisitos estáticos, se conforman a sí mismos.Lo siguiente compila muy bien:
bazrequiere que seTajuste aP; pero podemos sustituir enPparaT, porquePno 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
@objcprotocolos. El compilador puede simplificar significativamente el código genérico escrito alrededor de ellos.¿Por qué? Debido a que
@objclos valores de protocolo-tecleado son referencias efectivamente acaba de clase cuyos requisitos se envían utilizandoobjc_msgSend. Por otro lado, los@objcvalores 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
@objcprotocolos, un valor de dicho tipo de protocoloPpuede 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.@objcSin 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
@objcprotocolos, 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
@objcprotocolos.Pero, ¿qué soluciones actuales hay para los no
@objcprotocolos?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 : Py simplemente avanzar a la== Pextensió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
patakesConcreteP(_:), ya que actualmente no podemos sustituirPa 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
PporT : P, qué pasaría si pudiéramos cavar en el tipo de hormigón subyacente de que elPvalor 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
Selfmarcador de posición genérico implícito que toma el método de extensión, que se utiliza para escribir elselfpará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 elSelfmarcador de posición genérico. Es por esto que estamos en condiciones de llamartakesConcreteP(_:)aself- estamos satisfaciendoTconSelf.Esto significa que ahora podemos decir:
Y
takesConcreteP(_:)se llama con su marcador de posición genéricoTsatisfecho 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
Tparámetros también sería problemática, ya que los parámetros deben tomar argumentos del mismo tipo; sin embargo, si tenemos dosPvalores, 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
Plos requisitos de instancia a una instancia arbitraria subyacente que se ajuste aP:Ahora podemos hablar en términos de en
AnyPlugar 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
Ptuviera un requisito estático: habríamos tenido que implementarlo enAnyP. Pero, ¿cómo debería haberse implementado? Estamos lidiando con instancias arbitrarias que se conformanPaquí: 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
Pcomo 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.Typemetatipos). El problema es que para los genéricos, en realidad no estamos comparando lo mismo. CuandoTes asíP, no hay ningún tipo de hormigón subyacente (meta) para reenviar los requisitos estáticos a (Tes unP.Protocol, no unP.Type) ...Si extiende el
CollectionTypeprotocolo en lugar de unaArrayrestricción por protocolo como un tipo concreto, puede volver a escribir el código anterior de la siguiente manera.fuente
== Pvs: 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 definoarrcomo[SubP]entoncesarr.test()ya no funcionarán (error: SUBP y P deben ser equivalentes).