swift - leguminosarum - El protocolo no se ajusta a sí mismo?
rhizobium leguminosarum (3)
¿Por qué no se 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 el arreglo
arr
como un arreglo
de un tipo de protocolo
, pero ¿es eso algo irrazonable?
¿Pensé que los protocolos estaban allí exactamente para ayudar a suministrar estructuras con algo así como una jerarquía de tipos?
¿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:
-
métodos
static
y propiedades - Iniciadores
- Tipos asociados (aunque estos actualmente evitan el uso de un protocolo como tipo real)
Podemos acceder a estos requisitos en un marcador de posición genérico
T
donde
T : 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 permitir que
T
sea
P
Considere lo que sucedería en el siguiente ejemplo si permitiéramos que la extensión
Array
sea aplicable a
[P]
:
protocol P {
init()
}
struct S : P {}
struct S1 : P {}
extension Array where Element : P {
mutating func appendNew() {
// If Element is P, we cannot possibly construct a new instance of it, as you cannot
// construct an instance of a protocol.
append(Element())
}
}
var arr: [P] = [S(), S1()]
// error: Using ''P'' as a concrete type conforming to protocol ''P'' is not supported
arr.appendNew()
No podemos llamar a
appendNew()
en un
[P]
, porque
P
(el
Element
) 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 ajusta a
P
Es una historia similar con el método estático y los requisitos de propiedad:
protocol P {
static func foo()
static var bar: Int { get }
}
struct SomeGeneric<T : P> {
func baz() {
// If T is P, what''s the value of bar? There isn''t one – because there''s no
// implementation of bar''s getter defined on P itself.
print(T.bar)
T.foo() // If T is P, what method are we calling here?
}
}
// error: Using ''P'' as a concrete type conforming to protocol ''P'' is not supported
SomeGeneric<P>().baz()
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 de
foo()
o
bar
definidas en el ejemplo anterior).
Aunque podemos definir implementaciones de estos requisitos en una extensión
P
, estos se definen solo para los tipos concretos que se ajustan a
P
; aún no puede invocarlos en
P
mismo.
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 del tipo concreto subyacente de ese requisito.
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
associatedtype
requisitos de tipo
associatedtype
, 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 idioma
Editar: y como se explora a continuación, esto se parece a lo que el equipo Swift está buscando.
Protocolos
@objc
Y, de hecho, así es
exactamente
como el lenguaje trata los protocolos
@objc
.
Cuando no tienen requisitos estáticos, se conforman a sí mismos.
Lo siguiente compila muy bien:
import Foundation
@objc protocol P {
func foo()
}
class C : P {
func foo() {
print("C''s foo called!")
}
}
func baz<T : P>(_ t: T) {
t.foo()
}
let c: P = C()
baz(c)
baz
requiere que
T
ajuste a
P
;
pero podemos sustituir en
P
por
T
porque
P
no tiene requisitos estáticos.
Si agregamos un requisito estático a
P
, el ejemplo ya no se compila:
import Foundation
@objc protocol P {
static func bar()
func foo()
}
class C : P {
static func bar() {
print("C''s bar called")
}
func foo() {
print("C''s foo called!")
}
}
func baz<T : P>(_ t: T) {
t.foo()
}
let c: P = C()
baz(c) // error: Cannot invoke ''baz'' with an argument list of type ''(P)''
Entonces, una solución a este problema es hacer que 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 los protocolos
@objc
.
El compilador puede simplificar significativamente el código genérico escrito alrededor de ellos.
¿Por qué?
Debido a que los
@objc
protocolo
@objc
son efectivamente solo referencias de clase cuyos requisitos se envían utilizando
objc_msgSend
.
Por otro lado, los valores tipificados por protocolo que no son
@objc
son más complicados, ya que transportan tanto tablas de valores como testigos para administrar la memoria de su valor empaquetado (potencialmente almacenado indirectamente) y determinar qué implementaciones solicitar los diferentes requisitos, respectivamente.
Debido a esta representación simplificada para los protocolos
@objc
, un valor de dicho tipo de protocolo
P
puede compartir la misma representación de memoria que un ''valor genérico'' de tipo algún marcador de posición genérico
T : P
,
presumiblemente
facilitando que el equipo de Swift permita autoconformidad.
Sin embargo, lo mismo no es cierto para los protocolos que no son
@objc
, ya que estos 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 se implemente en protocolos que no sean
@objc
, 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
):
Matt Neuburg agregó un comentario - 7 Sep 2017 1:33 PM
Esto compila:
@objc protocol P {} class C: P {} func process<T: P>(item: T) -> T { return item } func f(image: P) { let processed: P = process(item:image) }
Agregar
@objc
hace compilar; eliminarlo hace que no se vuelva a compilar. A algunos de nosotros en nos parece sorprendente y nos gustaría saber si es un caso deliberado o con errores.Slava Pestov agregó un comentario - 7 Sep 2017 1:53 PM
Es deliberado: levantar esta restricción es de lo que se trata este error. Como dije, es complicado y todavía no tenemos planes concretos.
Espero que sea algo que el lenguaje algún día también admitirá para protocolos que no sean
@objc
.
Pero, ¿qué soluciones actuales existen para los protocolos que no son
@objc
?
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 determinado o un tipo asociado debe ser un tipo de protocolo determinado (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:
extension Array where Element == P {
func test<T>() -> [T] {
return []
}
}
let arr: [P] = [S()]
let result: [S] = arr.test()
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 cuando
Element : P
, y simplemente avanzar a la extensión
== P
:
extension Array where Element : P {
func test<T>() -> [T] {
return (self as [P]).test()
}
}
let arr = [S()]
let result: [S] = arr.test()
Sin embargo, vale la pena señalar que esto realizará una conversión de O (n) de la matriz a una
[P]
, ya que cada elemento tendrá que estar encuadrado 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 envoltura para una
[P]
, en la 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):
protocol P {
var bar: Int { get set }
func foo(str: String)
}
struct S : P {
var bar: Int
func foo(str: String) {/* ... */}
}
func takesConcreteP<T : P>(_ t: T) {/* ... */}
let p: P = S(bar: 5)
// error: Cannot invoke ''takesConcreteP'' with an argument list of type ''(P)''
takesConcreteP(p)
No podemos pasar
p
a
takesConcreteP(_:)
, ya que actualmente no podemos sustituir
P
por un marcador de posición genérico
T : P
Echemos un vistazo a un par de formas en que podemos resolver este problema.
1. Aperturas existenciales
En lugar de intentar sustituir
P
por
T : P
, ¿qué pasaría si pudiéramos profundizar en el tipo de hormigón subyacente que el valor tipado
P
estaba envolviendo y sustituir eso?
Desafortunadamente, esto requiere una función de lenguaje llamada
existenciales de apertura
, que actualmente no está disponible directamente para los usuarios.
Sin embargo, Swift abre implícitamente existenciales (valores tipificados por protocolo) cuando accede a miembros en ellos (es decir, extrae el tipo de tiempo de ejecución y lo hace accesible en forma de un marcador de posición genérico).
Podemos explotar este hecho en una extensión de protocolo en
P
:
extension P {
func callTakesConcreteP/*<Self : P>*/(/*self: Self*/) {
takesConcreteP(self)
}
}
Tenga en cuenta el marcador de posición genérico implícito
Self
que toma el método de extensión, que se utiliza para escribir el parámetro
self
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 escrito
P
, Swift desentierra el tipo concreto subyacente y lo utiliza para satisfacer el marcador de posición genérico
Self
.
Es por eso que podemos llamar a
takesConcreteP(_:)
con
self
; estamos satisfaciendo
T
con
Self
.
Esto significa que ahora podemos decir:
p.callTakesConcreteP()
Y se
takesConcreteP(_:)
con su marcador de posición genérico
T
satisfecho por el tipo concreto subyacente (en este caso
S
).
Tenga en cuenta que esto no es "protocolos que se ajustan a sí mismos", ya que estamos sustituyendo un tipo concreto en lugar de
P
: intente agregar un requisito estático al protocolo y ver qué sucede cuando lo llama desde
takesConcreteP(_:)
.
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, haciendo exactamente lo que hizo nuestra extensión de protocolo de trampolín, solo sin la repetitiva placa.
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:
struct Q : P {
var bar: Int
func foo(str: String) {}
}
// The placeholder `T` must be satisfied by a single type
func takesConcreteArrayOfP<T : P>(_ t: [T]) {}
// ...but an array of `P` could have elements of different underlying concrete types.
let array: [P] = [S(bar: 1), Q(bar: 2)]
// So there''s no sensible concrete type we can substitute for `T`.
takesConcreteArrayOfP(array)
Por las mismas razones, una función con múltiples parámetros
T
también sería problemática, ya que los parámetros deben tomar argumentos del mismo tipo; sin embargo, si tenemos dos valores
P
, no hay forma de garantizar en el momento de la compilación que ambos tengan el mismo tipo de hormigón subyacente.
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 los requisitos de instancia de
P
a una instancia arbitraria subyacente que se ajuste a
P
:
struct AnyP : P {
private var base: P
init(_ base: P) {
self.base = base
}
var bar: Int {
get { return base.bar }
set { base.bar = newValue }
}
func foo(str: String) { base.foo(str: str) }
}
Ahora podemos hablar en términos de
AnyP
lugar de
P
:
let p = AnyP(S(bar: 5))
takesConcreteP(p)
// example from #1...
let array = [AnyP(S(bar: 1)), AnyP(Q(bar: 2))]
takesConcreteArrayOfP(array)
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
tenía un requisito estático; habríamos tenido que implementarlo en
AnyP
.
Pero, ¿cómo debería haberse implementado?
Estamos lidiando con instancias arbitrarias que se ajustan a
P
aquí: no sabemos cómo sus tipos concretos subyacentes implementan los requisitos estáticos, por lo tanto, no podemos expresar esto de manera significativa en
AnyP
.
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 a
P
como un tipo concreto que se ajusta a
P
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:
"No se admite el uso de ''P'' como tipo concreto conforme al protocolo ''P''".
Eso realmente hace que todo esto sea mucho más claro. Esta extensión:
extension Array where Element : P {
no se aplica cuando
Element == P
ya que
P
no se considera una conformidad concreta de
P
(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 conocido para
P
).
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 que
Array<P>
pueda ser de tipo variable (donde
Array
no puede) indica que ya han hecho algo de trabajo en esta dirección, pero los metatipos Swift tiene muchos bordes afilados y cajas 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.
protocol P { }
struct S: P { }
struct AnyPArray {
var array: [P]
init(_ array:[P]) { self.array = array }
}
extension AnyPArray {
func test<T>() -> [T] {
return []
}
}
let arr = AnyPArray([S()])
let result: [S] = arr.test()
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.
Tuviste que encajonarlos y fue increíblemente molesto y restrictivo, y finalmente el compilador agregó
indirect
para hacer lo mismo más automáticamente.
Si extiende el protocolo
CollectionType
lugar de
Array
y la restricción por protocolo como un tipo concreto, puede volver a escribir el código anterior de la siguiente manera.
protocol P { }
struct S: P { }
let arr:[P] = [ S() ]
extension CollectionType where Generator.Element == P {
func test<T>() -> [T] {
return []
}
}
let result : [S] = arr.test()