Funkcja protokołu zwracająca Self


82

Mam protokół P, który zwraca kopię obiektu:

protocol P {
    func copy() -> Self
}

i klasa C, która implementuje P:

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

Jednak czy umieszczę wartość zwracaną, gdy Selfotrzymam następujący błąd:

Nie można przekonwertować wyrażenia zwrotnego typu „C” na zwrot typu „Self”

Próbowałem też wrócić C.

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

Spowodowało to następujący błąd:

Metoda „copy ()” w nie ostatecznej klasie „C” musi powrócić Selfdo zgodności z protokołem „P”

Nic nie działa z wyjątkiem przypadku, w którym przedrostek class Cz finalie do:

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

Jednak jeśli chcę podklasę C, nic nie zadziała. Czy można to obejść?


1
Co masz na myśli mówiąc „nic nie działa”?
Rob Napier,

Kompilator skarży się, umieszczając C lub Self jako wartość zwracaną, chyba że classjest to afinal class
aeubanks

6
OK, odtworzyłem błędy, ale zadając pytania, musisz podać rzeczywisty zwracany błąd. Nie tylko „powoduje błędy” lub „nie działa”.
Rob Napier,

Kompilator jest tutaj całkowicie poprawny w swoich błędach, BTW. Zastanawiam się tylko, czy w ogóle możesz dostać to, co próbujesz zrobić.
Rob Napier,

1
Ale możesz zadzwonić [[[self class] alloc] init]. Więc myślę, że pytanie brzmi, czy istnieje bezpieczny dla typu sposób wywołania bieżącej klasy i wywołania metody init?
aeubanks

Odpowiedzi:


144

Problem polega na tym, że obiecujesz, że kompilator nie może udowodnić, że dotrzymasz.

Więc stworzyłeś tę obietnicę: wywołanie copy()zwróci swój własny typ, w pełni zainicjowany.

Ale potem wdrożyłeś w copy()ten sposób:

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

Teraz jestem podklasą, która nie zastępuje copy(). I zwracam C, nie w pełni zainicjalizowany Self(co obiecałem). Więc to nie jest dobre. Co powiesz na:

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

Cóż, to się nie skompiluje, ale nawet gdyby tak się stało, nie byłoby dobrze. Podklasa może nie mieć trywialnego konstruktora, więc D()może nawet nie być legalna. (Chociaż patrz poniżej.)

OK, a co powiesz na:

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

Tak, ale to nie wraca Self. Wraca C. Nadal nie dotrzymujesz obietnicy.

"Ale ObjC może to zrobić!" Cóż, w pewnym sensie. Głównie dlatego, że nie obchodzi go, czy dotrzymasz obietnicy tak, jak robi to Swift. Jeśli nie uda ci się zaimplementować copyWithZone:w podklasie, możesz nie zainicjować pełnego obiektu. Kompilator nawet nie ostrzeże Cię, że to zrobiłeś.

„Ale prawie wszystko w ObjC można przetłumaczyć na Swift, a ObjC tak NSCopying”. Tak, i oto jak to zdefiniowano:

func copy() -> AnyObject!

Możesz więc zrobić to samo (nie ma powodu! Tutaj):

protocol Copyable {
  func copy() -> AnyObject
}

To mówi: „Nie obiecuję niczego, co otrzymasz z powrotem”. Możesz też powiedzieć:

protocol Copyable {
  func copy() -> Copyable
}

To obietnica, którą możesz złożyć.

Ale możemy myśleć o C ++ na chwilę i pamiętać, że jest to obietnica, że można zrobić. Możemy obiecać, że my i wszystkie nasze podklasy zaimplementujemy określone rodzaje inicjatorów, a Swift wymusi to (i może udowodnić, że mówimy prawdę):

protocol Copyable {
  init(copy: Self)
}

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

I tak należy wykonywać kopie.

Możemy pójść o krok dalej, ale używa go dynamicTypei nie testowałem go szczegółowo, aby upewnić się, że zawsze jest to, czego chcemy, ale powinno być poprawne:

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.
  }
}

Tutaj obiecujemy, że istnieje inicjator, który wykonuje dla nas kopie, a następnie możemy w czasie wykonywania określić, który z nich wywołać, podając nam składnię metody, której szukasz.


Hmm, musieli to zmienić. Mógłbym przysiąc, że to func copy() -> Cdziałało w poprzednich wersjach beta i było spójne, ponieważ zgodność z protokołem nie była dziedziczona. (Teraz wydaje się, że zgodność protokołu jest dziedziczona i func copy() -> Cnie działa.)
newacct

2
Ostatnie rozwiązanie Pure-Swift nie działa z podklasami, ponieważ init(copy: C)zamiast tego są one wymagane init(copy: Self):(
fluidsonic

Ostatnie rozwiązanie gwarantuje, że wartość zwracana będzie, Selfale inicjator musi wtedy zaakceptować zmienną wpisaną statycznie, Cco oznacza, że ​​po prostu zwracanie nie jest zbytnim ulepszeniem AnyObject.
chakrit

1
W swift 2.0 musiałbyś wywołać init wprost:self.dynamicType.init( ... )
Ben Affleck

1
@Dschee wewnątrz C, Self mogłoby być C lub podklasą C. To są różne typy.
Rob Napier

25

W Swift 2 możemy do tego użyć rozszerzeń protokołów.

protocol Copyable {
    init(copy:Self)
}

extension Copyable {
    func copy() -> Self {
        return Self.init(copy: self)
    }
}

To świetna odpowiedź i tego typu podejście zostało szeroko omówione podczas WWDC 2015.
gkaimakas

2
To powinna być akceptowana odpowiedź. Można to uprościć return Self(copy: self)(przynajmniej w Swift 2.2).
jhrmnn

16

Jest inny sposób robienia tego, co chcesz, polegający na wykorzystaniu skojarzonego z nim typu języka Swift. Oto prosty przykład:

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()

Fascynujący. Zastanawiam się, czy to odnosi się do stackoverflow.com/q/42041150/294884
Fattie,

Ten robi to, co mnie interesuje. Dzięki!
Josh w The Nerdery

10

Właściwie istnieje sztuczka, która pozwala łatwo wrócić, Selfgdy wymaga tego protokół ( streszczenie ):

/// 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

1
Łał. kompiluje. To podstępne, ponieważ kompilator nie pozwoli ci po prostureturn Vehicle() as! Self
SimplGy

to jest oszałamiające. Łał. Czy to, o co tutaj pytam, faktycznie jest odmianą tego? stackoverflow.com/q/42041150/294884
Fattie

@JoeBlow Obawiam się, że tak nie jest. Powiedziałbym, że aby zachować bezpieczeństwo umysłu, powinniśmy dokładnie znać zwracany typ (tj. Nie „A lub B”, ale tylko „A”; w przeciwnym razie musimy pomyśleć o polimorfizmie + dziedziczeniu + przeciążeniu funkcji (przynajmniej).
werediver

to sztuczka kompilatora. Ponieważ przesłanianie foo()nie jest wymuszane, każdy Vehicleelement podrzędny bez foo()niestandardowej implementacji spowoduje oczywistą awarię autocast(). Na przykład: class SuperCar: Vehicle { } let superCar = SuperCar.foo() . Instancji Vehiclenie można sprowadzić do SuperCar- więc wymuś rozpakowanie nil w „autocast ()” prowadzi do awarii.
freennnn

1
@freennnn Zmiana kodu na następujący nie powoduje awarii, gdy podklasa nie zastępuje foo(). Jedynym wymaganiem jest to, że klasa Foomusi mieć wymagany inicjator, aby to działało, jak pokazano poniżej. 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

Zgodnie z sugestią Roba można to uczynić bardziej ogólnym z powiązanymi typami . Zmieniłem nieco przykład, aby pokazać korzyści płynące z tego podejścia.

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)
    }
}

1

Miałem podobny problem i wymyśliłem coś, co może być przydatne, więc pomyślałem, że podzielę się tym na przyszłość, ponieważ jest to jedno z pierwszych miejsc, które znalazłem, szukając rozwiązania.

Jak wspomniano powyżej, problemem jest niejednoznaczność zwracanego typu funkcji copy (). Można to bardzo wyraźnie zilustrować, oddzielając funkcje copy () -> C i copy () -> P:

Więc zakładając, że zdefiniujesz protokół i klasę w następujący sposób:

protocol P
{
   func copy() -> P
}

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

To kompiluje i daje oczekiwane wyniki, gdy typ wartości zwracanej jest jawny. Za każdym razem, gdy kompilator musi samodzielnie zdecydować, jaki powinien być zwracany typ (samodzielnie), uzna sytuację za niejednoznaczną i zakończy się niepowodzeniem dla wszystkich klas konkretnych implementujących protokół P.

Na przykład:

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

Podsumowując, zadziałałoby to w sytuacjach, w których albo nie używasz funkcji copy () klasy bazowej, albo zawsze masz jawny kontekst typu.

Zauważyłem, że używając tej samej nazwy funkcji, co klasa konkretna stworzona dla nieporęcznego kodu wszędzie, więc ostatecznie użyłem innej nazwy dla funkcji copy () protokołu.

Efekt końcowy jest bardziej podobny do:

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() }       
}

Oczywiście mój kontekst i funkcje są zupełnie inne, ale w duchu pytania starałem się trzymać jak najbliżej podanego przykładu.


1

Swift 5.1 pozwala teraz na wymuszone rzucenie na siebie, 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

0

Tylko wrzucam swój kapelusz do ringu. Potrzebowaliśmy protokołu, który zwrócił opcjonalny typ, na którym zastosowano protokół. Chcieliśmy również, aby przesłonięcie jawnie zwracało typ, a nie tylko Self.

Sztuczka polega na tym, że zamiast używać „Self” jako typu zwracanego, zamiast tego należy zdefiniować powiązany typ, który ustawiasz jako równy Self, a następnie użyć tego skojarzonego typu.

Oto stary sposób, używając Self ...

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

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

Oto nowy sposób korzystania z powiązanego typu. Zwróć uwagę, że zwracany typ jest teraz jawny, a nie „Self”.

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

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

0

Aby dodać do odpowiedzi associatedtypesposób, proponuję przenieść tworzenie instancji na domyślną implementację rozszerzenia protokołu. W ten sposób klasy zgodne nie będą musiały go implementować, oszczędzając w ten sposób duplikacji kodu:

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()
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.