Protokollfunktion, die Selbst zurückgibt


81

Ich habe ein Protokoll P, das eine Kopie des Objekts zurückgibt:

protocol P {
    func copy() -> Self
}

und eine Klasse C, die P implementiert:

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

Ob ich jedoch den Rückgabewert setze, Selferhalte ich den folgenden Fehler:

Der Rückgabeausdruck vom Typ 'C' kann nicht in den Rückgabetyp 'Self' konvertiert werden.

Ich habe auch versucht zurückzukehren C.

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

Dies führte zu folgendem Fehler:

Die Methode 'copy ()' in der nicht endgültigen Klasse 'C' muss zurückkehren Self, um dem Protokoll 'P' zu entsprechen.

Nichts funktioniert , außer für den Fall, dass ich das Präfix class Cmit finalalso zB:

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

Wenn ich jedoch C unterordnen möchte, würde nichts funktionieren. Gibt es einen Weg daran vorbei?


1
Was meinst du mit "nichts funktioniert"?
Rob Napier

Der Compiler beschwert sich, wenn er entweder C oder Self als Rückgabewert setzt, es sei denn, das classist afinal class
aeubanks

6
OK, ich habe die Fehler reproduziert, aber wenn Sie Fragen stellen, müssen Sie den tatsächlichen Fehler angeben, der zurückgegeben wird. Nicht nur "es gibt Fehler" oder "es funktioniert nicht".
Rob Napier

Der Compiler ist in seinen Fehlern hier übrigens völlig korrekt. Ich denke nur darüber nach, ob Sie das bekommen können, was Sie überhaupt versuchen.
Rob Napier

1
Aber du kannst anrufen [[[self class] alloc] init]. Die Frage ist also, ob es eine typsichere Möglichkeit gibt, die aktuelle Klasse aufzurufen und eine init-Methode aufzurufen.
Aeubanks

Antworten:


144

Das Problem ist, dass Sie ein Versprechen abgeben, dass der Compiler nicht beweisen kann, dass Sie es halten werden.

Sie haben dieses Versprechen erstellt: Calling copy()gibt seinen eigenen Typ zurück, der vollständig initialisiert ist.

Aber dann haben Sie Folgendes implementiert copy():

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

Jetzt bin ich eine Unterklasse, die nicht überschreibt copy(). Und ich gebe ein zurück C, kein vollständig initialisiertes Self(was ich versprochen habe). Das ist also nicht gut. Wie wäre es mit:

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

Nun, das wird nicht kompiliert, aber selbst wenn es so wäre, wäre es nicht gut. Die Unterklasse hat möglicherweise keinen trivialen Konstruktor und ist daher D()möglicherweise nicht einmal legal. (Siehe unten.)

OK, wie wäre es mit:

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

Ja, aber das kommt nicht zurück Self. Es kehrt zurück C. Du hältst dein Versprechen immer noch nicht.

"Aber ObjC kann es schaffen!" Naja, so ungefähr. Meistens, weil es egal ist, ob Sie Ihr Versprechen so halten wie Swift. Wenn Sie die Implementierung copyWithZone:in der Unterklasse nicht durchführen können, können Sie Ihr Objekt möglicherweise nicht vollständig initialisieren. Der Compiler warnt Sie nicht einmal davor, dass Sie das getan haben.

"Aber fast alles in ObjC kann in Swift übersetzt werden, und ObjC hat es NSCopying." Ja, und so wird es definiert:

func copy() -> AnyObject!

Sie können also dasselbe tun (hier gibt es keinen Grund dafür!):

protocol Copyable {
  func copy() -> AnyObject
}

Das heißt "Ich verspreche nichts darüber, was Sie zurückbekommen." Man könnte auch sagen:

protocol Copyable {
  func copy() -> Copyable
}

Das ist ein Versprechen, das du machen kannst.

Aber wir können eine Weile über C ++ nachdenken und uns daran erinnern, dass wir ein Versprechen geben können . Wir können versprechen, dass wir und alle unsere Unterklassen bestimmte Arten von Initialisierern implementieren werden, und Swift wird dies durchsetzen (und so beweisen, dass wir die Wahrheit sagen):

protocol Copyable {
  init(copy: Self)
}

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

Und so sollten Sie Kopien erstellen.

Wir können noch einen Schritt weiter gehen, aber es wird verwendet dynamicType, und ich habe es nicht ausführlich getestet, um sicherzustellen, dass dies immer das ist, was wir wollen, aber es sollte korrekt sein:

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

Hier versprechen wir, dass es einen Initialisierer gibt, der Kopien für uns ausführt, und dann können wir zur Laufzeit bestimmen, welcher aufgerufen werden soll, und uns die von Ihnen gesuchte Methodensyntax geben.


Hmm, das müssen sie geändert haben. Ich hätte schwören können, dass dies func copy() -> Cin früheren Betas funktioniert hat, und es war konsistent, weil die Protokollkonformität nicht vererbt wurde. (Jetzt scheint es, dass die Protokollkonformität vererbt wird und func copy() -> Cnicht funktioniert.)
newacct

2
Die letzte Pure-Swift-Lösung funktioniert nicht mit Unterklassen, da diese init(copy: C)stattdessen implementiert werden müssen init(copy: Self):(
fluidsonic

Die letzte Lösung garantiert, dass der Rückgabewert ist, Selfaber der Initialisierer muss dann alle eine statisch typisierte Variable akzeptieren, Cdas heißt, es ist keine große Verbesserung, nur AnyObjecterst zurückzukehren.
Chakrit

1
In Swift 2.0 self.dynamicType.init( ... )
müssten

1
@Dschee innerhalb von C, Self könnte C oder eine Unterklasse von C sein. Das sind verschiedene Typen.
Rob Napier

24

Mit Swift 2 können wir hierfür Protokollerweiterungen verwenden.

protocol Copyable {
    init(copy:Self)
}

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

Dies ist eine großartige Antwort und diese Art von Ansatz wurde auf der WWDC 2015 ausführlich diskutiert.
Gkaimakas

2
Dies sollte die akzeptierte Antwort sein. Es kann mit vereinfacht werden return Self(copy: self)(zumindest in Swift 2.2).
Jhrmnn

16

Es gibt eine andere Möglichkeit, das zu tun, was Sie möchten, indem Sie den zugehörigen Typ von Swift nutzen. Hier ist ein einfaches Beispiel:

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

Faszinierend. Ich frage mich, ob das mit stackoverflow.com/q/42041150/294884
Fattie

Dieser macht das, woran ich interessiert bin. Danke!
Josh im Nerdery

10

Tatsächlich gibt es einen Trick, mit dem Sie problemlos zurückkehren können, Selfwenn dies für ein Protokoll erforderlich ist :

/// 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
Beeindruckend. kompiliert. Das ist knifflig, denn der Compiler lässt Sie nicht einfachreturn Vehicle() as! Self
SimplGy

das ist umwerfend. Beeindruckend. Ist das, was ich hier frage, tatsächlich eine Variation davon? stackoverflow.com/q/42041150/294884
Fattie

@ JoeBlow Ich fürchte, das ist es nicht. Ich würde sagen, um unseren Verstand zu schützen, sollten wir den Rückgabetyp genau kennen (dh nicht "A oder B", sondern nur "A"; andernfalls müssen wir (zumindest) über Polymorphismus + Vererbung + Funktionsüberladung nachdenken.
werediver

Das ist Compiler-Trick. Da das Überschreiben von foo()nicht erzwungen wird, führt jeder VehicleNachkomme ohne foo()benutzerdefinierte Implementierung zu einem offensichtlichen Absturz autocast(). Zum Beispiel:class SuperCar: Vehicle { } let superCar = SuperCar.foo() . Die Instanz von Vehiclekann nicht herabgestuft werden SuperCar- daher erzwingt das erzwungene Auspacken von Null in 'autocast ()' einen Absturz.
Freennnn

1
@freennnn Das Ändern des Codes in den folgenden Code stürzt nicht ab, wenn eine Unterklasse nicht überschrieben wird foo(). Die einzige Voraussetzung ist, dass die Klasse Fooüber einen erforderlichen Initialisierer verfügt, damit dies wie unten gezeigt funktioniert. 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

Auf Robs Vorschlag hin könnte dies mit den zugehörigen Typen allgemeiner gestaltet werden . Ich habe das Beispiel ein wenig geändert, um die Vorteile des Ansatzes zu demonstrieren.

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

Ich hatte ein ähnliches Problem und habe mir etwas ausgedacht, das nützlich sein könnte. Ich dachte, ich würde es als zukünftige Referenz weitergeben, da dies einer der ersten Orte ist, die ich bei der Suche nach einer Lösung gefunden habe.

Wie oben erwähnt, liegt das Problem in der Mehrdeutigkeit des Rückgabetyps für die Funktion copy (). Dies kann sehr deutlich veranschaulicht werden, indem die Funktionen copy () -> C und copy () -> P getrennt werden:

Angenommen, Sie definieren das Protokoll und die Klasse wie folgt:

protocol P
{
   func copy() -> P
}

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

Dies kompiliert und erzeugt die erwarteten Ergebnisse, wenn der Typ des Rückgabewerts explizit ist. Jedes Mal, wenn der Compiler (allein) entscheiden muss, wie der Rückgabetyp lauten soll, wird die Situation nicht eindeutig und schlägt für alle konkreten Klassen fehl, die das P-Protokoll implementieren.

Zum Beispiel:

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

Zusammenfassend lässt sich sagen, dass dies in Situationen funktioniert, in denen Sie entweder die copy () - Funktion der Basisklasse nicht verwenden oder immer einen expliziten Typkontext haben.

Ich fand heraus, dass die Verwendung des gleichen Funktionsnamens wie die konkrete Klasse überall für unhandlichen Code gemacht wurde, und verwendete daher einen anderen Namen für die copy () - Funktion des Protokolls.

Das Endergebnis ist eher wie folgt:

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

Natürlich sind mein Kontext und meine Funktionen völlig unterschiedlich, aber im Geiste der Frage habe ich versucht, dem gegebenen Beispiel so nahe wie möglich zu kommen.


0

Ich werfe hier nur meinen Hut in den Ring. Wir brauchten ein Protokoll, das ein optionales Protokoll des Typs zurückgab, auf den das Protokoll angewendet wurde. Wir wollten auch, dass die Überschreibung den Typ explizit zurückgibt, nicht nur Self.

Der Trick besteht darin, anstatt 'Self' als Rückgabetyp zu verwenden, stattdessen einen zugeordneten Typ zu definieren, den Sie auf Self setzen, und dann diesen zugeordneten Typ zu verwenden.

Hier ist der alte Weg, mit Selbst ...

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

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

Hier ist der neue Weg mit dem zugehörigen Typ. Beachten Sie, dass der Rückgabetyp jetzt explizit ist und nicht 'Self'.

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

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

0

Um die Antworten auf diese associatedtypeWeise zu ergänzen , schlage ich vor, die Erstellung der Instanz auf eine Standardimplementierung der Protokollerweiterung zu verschieben. Auf diese Weise müssen die konformen Klassen es nicht implementieren, wodurch Code-Duplikationen vermieden werden:

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

0

Swift 5.1 erlaubt jetzt eine erzwungene Besetzung von Self, 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
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.