¿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 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 T
ser P
.
Considere lo que sucedería en el siguiente ejemplo si permitiéramos que la Array
extensión 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 recurrir appendNew()
a a [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 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). Si bien podemos definir implementaciones de estos requisitos en una P
extensión, estos se definen solo para los tipos concretos que se ajustan a ellos P
; aún no puede P
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 idioma
Editar: y como se explora a continuación, esto se parece a lo que el equipo Swift está buscando.
@objc
protocolos
Y, 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:
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 se T
ajuste a P
; pero podemos sustituir en P
para 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 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 utilizando objc_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 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 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 ):
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 lo @objc
hace compilar; eliminarlo hace que no se vuelva a compilar. Algunos de nosotros en Stack Overflow encuentran esto sorprendente y nos gustaría saber si eso es deliberado o un caso marginal con errores.
Slava Pestov agregó un comentario - 7 sep 2017 1:53 PM
Es deliberado: eliminar 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 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:
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 cuándo Element : P
y simplemente avanzar a la == P
extensión:
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 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):
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
a 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 cavar en el tipo de hormigón subyacente de que el P
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
:
extension P {
func callTakesConcreteP/*<Self : P>*/(/*self: Self*/) {
takesConcreteP(self)
}
}
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 el self
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 escrito P
, Swift desentierra el tipo concreto subyacente y lo utiliza para satisfacer el Self
marcador de posición genérico. Es por esto que estamos en condiciones de llamar takesConcreteP(_:)
a self
- estamos satisfaciendo T
con Self
.
Esto significa que ahora podemos decir:
p.callTakesConcreteP()
Y takesConcreteP(_:)
se llama con su marcador de posición genérico T
satisfecho por el tipo de hormigón 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 adentro 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, 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:
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 T
parámetros también sería problemática, ya que los parámetros deben tomar argumentos del mismo tipo; sin embargo, si tenemos dos P
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 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 en 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
tuviera 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 conforman 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 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 P
como un tipo concreto que se ajuste a P
.
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.