Función de protocolo que regresa Self

82

Tengo un protocolo P que devuelve una copia del objeto:

protocol P {
    func copy() -> Self
}

y una clase C que implementa P:

class C : P {
    func copy() -> Self {
        return C()
    }
}

Sin embargo, si pongo el valor de retorno, Selfobtengo el siguiente error:

No se puede convertir la expresión de retorno del tipo 'C' al tipo de retorno 'Self'

También intenté regresar C.

class C : P {
    func copy() -> C  {
        return C()
    }
}

Eso resultó en el siguiente error:

El método 'copy ()' en la clase no final 'C' debe volver Selfa ajustarse al protocolo 'P'

Obras nada, excepto para el caso en que prefijo class Ccon finalesto es hacer:

final class C : P {
    func copy() -> C  {
        return C()
    }
}

Sin embargo, si quiero una subclase C, nada funcionaría. ¿Hay alguna forma de evitar esto?

aeubanks
fuente
1
¿Qué quieres decir con "nada funciona"?
Rob Napier
El compilador se queja cuando pone C o Self como valor de retorno a menos que classsea ​​afinal class
aeubanks
6
OK, he reproducido los errores, pero al hacer preguntas, debe incluir el error real que se devuelve. No solo "da errores" o "no funciona".
Rob Napier
El compilador es completamente correcto en sus errores aquí, por cierto. Solo estoy pensando en si puedes conseguir lo que estás intentando hacer.
Rob Napier
1
Pero puedes llamar [[[self class] alloc] init]. Entonces, supongo que la pregunta es: ¿existe una forma segura de llamar a la clase actual y llamar a un método init?
aeubanks

Respuestas:

144

El problema es que está haciendo una promesa que el compilador no puede demostrar que cumplirá.

Entonces creaste esta promesa: la llamada copy()devolverá su propio tipo, completamente inicializado.

Pero luego lo implementó de copy()esta manera:

func copy() -> Self {
    return C()
}

Ahora soy una subclase que no se anula copy(). Y devuelvo a C, no completamente inicializado Self(lo que prometí). Entonces eso no es bueno. Qué tal si:

func copy() -> Self {
    return Self()
}

Bueno, eso no se compilará, pero incluso si lo hiciera, no sería bueno. La subclase puede no tener un constructor trivial, por lo que D()puede que ni siquiera sea legal. (Aunque vea más abajo).

OK, bueno, ¿qué tal:

func copy() -> C {
    return C()
}

Sí, pero eso no regresa Self. Vuelve C. Aún no estás cumpliendo tu promesa.

"¡Pero ObjC puede hacerlo!" Especie de. Sobre todo porque no le importa si mantienes tu promesa como lo hace Swift. Si no logra implementar copyWithZone:en la subclase, es posible que no pueda inicializar completamente su objeto. El compilador ni siquiera le advertirá que ha hecho eso.

"Pero casi todo en ObjC se puede traducir a Swift, y ObjC sí NSCopying". Sí, así es como se define:

func copy() -> AnyObject!

Entonces puedes hacer lo mismo (¡no hay razón para el! Aquí):

protocol Copyable {
  func copy() -> AnyObject
}

Eso dice "No prometo nada sobre lo que recibes". También podrías decir:

protocol Copyable {
  func copy() -> Copyable
}

Esa es una promesa que puedes hacer.

Pero podemos pensar en C ++ por un momento y recordar que hay una promesa que podemos hacer. Podemos prometer que nosotros y todas nuestras subclases implementaremos tipos específicos de inicializadores, y Swift lo hará cumplir (y así puede demostrar que estamos diciendo la verdad):

protocol Copyable {
  init(copy: Self)
}

class C : Copyable {
  required init(copy: C) {
    // Perform your copying here.
  }
}

Y así es como debes realizar copias.

Podemos dar un paso más, pero se usa dynamicType, y no lo he probado exhaustivamente para asegurarnos de que siempre sea lo que queremos, pero debería ser correcto:

protocol Copyable {
  func copy() -> Self
  init(copy: Self)
}

class C : Copyable {
  func copy() -> Self {
    return self.dynamicType(copy: self)
  }

  required init(copy: C) {
    // Perform your copying here.
  }
}

Aquí prometemos que hay un inicializador que realiza copias por nosotros, y luego podemos en tiempo de ejecución determinar cuál llamar, dándonos la sintaxis del método que estabas buscando.

Rob Napier
fuente
Hmm, deben haber cambiado esto. Podría haber jurado que func copy() -> Cfuncionó en versiones beta anteriores, y fue consistente porque la conformidad del protocolo no fue heredada. (Ahora parece que la conformidad del protocolo se hereda y func copy() -> Cno funciona).
newacct
2
La última solución pura-Swift no funciona con subclases, ya que deben implementarlas en su init(copy: C)lugar init(copy: Self):(
fluidsonic
La última solución garantiza que el valor de retorno sea, Selfpero el inicializador debe aceptar una variable escrita de forma estática, Ces decir, no es una gran mejora regresar AnyObjecten primer lugar.
chakrit
1
En swift 2.0 tendría que llamar a init explícitamente:self.dynamicType.init( ... )
pronebird
1
@Dschee dentro de C, Self podría ser C o una subclase de C. Esos son tipos diferentes.
Rob Napier
25

Con Swift 2, podemos usar extensiones de protocolo para esto.

protocol Copyable {
    init(copy:Self)
}

extension Copyable {
    func copy() -> Self {
        return Self.init(copy: self)
    }
}
Tolga Okur
fuente
Esta es una gran respuesta y ese tipo de enfoque se discutió ampliamente en WWDC 2015.
gkaimakas
2
Esta debería ser la respuesta aceptada. Se puede simplificar con return Self(copy: self)(al menos en Swift 2.2).
jhrmnn
16

Hay otra forma de hacer lo que quiere que implica aprovechar el tipo asociado de Swift. He aquí un ejemplo sencillo:

public protocol Creatable {

    associatedtype ObjectType = Self

    static func create() -> ObjectType
}

class MyClass {

    // Your class stuff here
}

extension MyClass: Creatable {

    // Define the protocol function to return class type
    static func create() -> MyClass {

         // Create an instance of your class however you want
        return MyClass()
    }
}

let obj = MyClass.create()
Matt Mendrala
fuente
Fascinante. Me pregunto si eso se relaciona con stackoverflow.com/q/42041150/294884
Fattie
Este hace lo que me interesa. ¡Gracias!
Josh en The Nerdery
10

En realidad, hay un truco que permite regresar fácilmenteSelf cuando lo requiera un protocolo ( gist ):

/// Cast the argument to the infered function return type.
func autocast<T>(some: Any) -> T? {
    return some as? T
}

protocol Foo {
    static func foo() -> Self
}

class Vehicle: Foo {
    class func foo() -> Self {
        return autocast(Vehicle())!
    }
}

class Tractor: Vehicle {
    override class func foo() -> Self {
        return autocast(Tractor())!
    }
}

func typeName(some: Any) -> String {
    return (some is Any.Type) ? "\(some)" : "\(some.dynamicType)"
}

let vehicle = Vehicle.foo()
let tractor = Tractor.foo()

print(typeName(vehicle)) // Vehicle
print(typeName(tractor)) // Tractor
werediver
fuente
1
Guau. compila. Eso es engañoso, porque el compilador no le permitirá simplementereturn Vehicle() as! Self
SimplGy
eso es alucinante. Guau. ¿Es lo que estoy preguntando aquí en realidad una variación de esto? stackoverflow.com/q/42041150/294884
Fattie
@JoeBlow Me temo que no lo es. Yo diría que para mantener nuestras mentes seguras, deberíamos saber exactamente el tipo de retorno (es decir, no "A o B", sino solo "A"; de lo contrario, debemos pensar en polimorfismo + herencia + sobrecarga de funciones (al menos).
werediver
eso es un engaño del compilador. Dado que foo()no se aplica la anulación de , cada Vehicledescendiente sin foo()una implementación personalizada producirá un bloqueo obvio en autocast(). Por ejemplo: class SuperCar: Vehicle { } let superCar = SuperCar.foo() . La instancia de Vehicleno se puede reducir a SuperCar, por lo que forzar el desenvolvimiento de nil en 'autocast ()' conduce al bloqueo.
freennnn
1
@freennnn Cambiar el código a lo siguiente no se bloquea cuando una subclase no se anula foo(). El único requisito es que la clase Foodebe tener un inicializador requerido para que esto funcione como se muestra a continuación. class Vehicle: Foo { public required init() { // Some init code here } class func foo() -> Self { return autocast(self.init())! // return autocast(Vehicle())! } } class Tractor: Vehicle { //Override is not necessary /*override class func foo() -> Self { return autocast(Tractor())! }*/ }
shawnynicole
2

Siguiendo la sugerencia de Rob, esto podría hacerse más genérico con los tipos asociados . Cambié un poco el ejemplo para demostrar los beneficios del enfoque.

protocol Copyable: NSCopying {
    associatedtype Prototype
    init(copy: Prototype)
    init(deepCopy: Prototype)
}
class C : Copyable {
    typealias Prototype = C // <-- requires adding this line to classes
    required init(copy: Prototype) {
        // Perform your copying here.
    }
    required init(deepCopy: Prototype) {
        // Perform your deep copying here.
    }
    @objc func copyWithZone(zone: NSZone) -> AnyObject {
        return Prototype(copy: self)
    }
}
David James
fuente
1

Tuve un problema similar y se me ocurrió algo que puede ser útil, así que pensé en compartirlo para referencia futura porque este es uno de los primeros lugares que encontré al buscar una solución.

Como se indicó anteriormente, el problema es la ambigüedad del tipo de retorno para la función copy (). Esto se puede ilustrar muy claramente separando las funciones copy () -> C y copy () -> P:

Entonces, asumiendo que define el protocolo y la clase de la siguiente manera:

protocol P
{
   func copy() -> P
}

class C:P  
{        
   func doCopy() -> C { return C() }       
   func copy() -> C   { return doCopy() }
   func copy() -> P   { return doCopy() }       
}

Esto compila y produce los resultados esperados cuando el tipo de valor de retorno es explícito. Cada vez que el compilador tiene que decidir cuál debe ser el tipo de retorno (por sí solo), encontrará la situación ambigua y fallará para todas las clases concretas que implementan el protocolo P.

Por ejemplo:

var aC:C = C()   // aC is of type C
var aP:P = aC    // aP is of type P (contains an instance of C)

var bC:C         // this to test assignment to a C type variable
var bP:P         //     "       "         "      P     "    "

bC = aC.copy()         // OK copy()->C is used

bP = aC.copy()         // Ambiguous. 
                       // compiler could use either functions
bP = (aC as P).copy()  // but this resolves the ambiguity.

bC = aP.copy()         // Fails, obvious type incompatibility
bP = aP.copy()         // OK copy()->P is used

En conclusión, esto funcionaría en situaciones en las que no estás usando la función copy () de la clase base o siempre tienes un contexto de tipo explícito.

Descubrí que usar el mismo nombre de función que la clase concreta generaba un código difícil de manejar en todas partes, así que terminé usando un nombre diferente para la función copy () del protocolo.

El resultado final es más parecido a:

protocol P
{
   func copyAsP() -> P
}

class C:P  
{
   func copy() -> C 
   { 
      // there usually is a lot more code around here... 
      return C() 
   }
   func copyAsP() -> P { return copy() }       
}

Por supuesto, mi contexto y funciones son completamente diferentes, pero en el espíritu de la pregunta, traté de mantenerme lo más cerca posible del ejemplo dado.

Alain T.
fuente
1

Swift 5.1 ahora permite un lanzamiento forzado a uno mismo, as! Self

  1> protocol P { 
  2.     func id() -> Self 
  3. } 
  9> class D : P { 
 10.     func id() -> Self { 
 11.         return D()
 12.     } 
 13. } 
error: repl.swift:11:16: error: cannot convert return expression of type 'D' to return type 'Self'
        return D()
               ^~~
                   as! Self


  9> class D : P { 
 10.     func id() -> Self { 
 11.         return D() as! Self
 12.     } 
 13. } //works
Johnlinvc
fuente
0

Solo tirando mi sombrero al ring aquí. Necesitábamos un protocolo que devolviera un opcional del tipo en el que se aplicó el protocolo. También queríamos que la anulación devolviera explícitamente el tipo, no solo Self.

El truco es en lugar de usar 'Self' como el tipo de retorno, en su lugar define un tipo asociado que establece igual a Self, luego usa ese tipo asociado.

Aquí está la forma antigua, usando Self ...

protocol Mappable{
    static func map() -> Self?
}

// Generated from Fix-it
extension SomeSpecificClass : Mappable{
    static func map() -> Self? {
        ...
    }
}

Aquí está la nueva forma de usar el tipo asociado. Tenga en cuenta que el tipo de retorno es explícito ahora, no 'Self'.

protocol Mappable{
    associatedtype ExplicitSelf = Self
    static func map() -> ExplicitSelf?
}

// Generated from Fix-it
extension SomeSpecificClass : Mappable{
    static func map() -> SomeSpecificClass? {
        ...
    }
}
Mark A. Donohoe
fuente
0

Para agregar a las respuestas con el associatedtypecamino, sugiero mover la creación de la instancia a una implementación predeterminada de la extensión del protocolo. De esa manera, las clases conformes no tendrán que implementarlo, evitando así la duplicación de código:

protocol Initializable {
    init()
}

protocol Creatable: Initializable {
    associatedtype Object: Initializable = Self
    static func newInstance() -> Object
}

extension Creatable {
    static func newInstance() -> Object {
        return Object()
    }
}

class MyClass: Creatable {
    required init() {}
}

class MyOtherClass: Creatable {
    required init() {}
}

// Any class (struct, etc.) conforming to Creatable
// can create new instances without having to implement newInstance() 
let instance1 = MyClass.newInstance()
let instance2 = MyOtherClass.newInstance()
Au Ris
fuente