Wie kann ich in Swift eine Variable eines bestimmten Typs deklarieren, die einem oder mehreren Protokollen entspricht?

96

In Swift kann ich den Typ einer Variablen explizit festlegen, indem ich ihn wie folgt deklariere:

var object: TYPE_NAME

Wenn wir noch einen Schritt weiter gehen und eine Variable deklarieren möchten, die mehreren Protokollen entspricht, können wir das protocolDeklarativ verwenden:

var object: protocol<ProtocolOne,ProtocolTwo>//etc

Was ist, wenn ich ein Objekt deklarieren möchte, das einem oder mehreren Protokollen entspricht und auch von einem bestimmten Basisklassentyp ist? Das Objective-C-Äquivalent würde folgendermaßen aussehen:

NSSomething<ABCProtocolOne,ABCProtocolTwo> * object = ...;

In Swift würde ich erwarten, dass es so aussieht:

var object: TYPE_NAME,ProtocolOne//etc

Dies gibt uns die Flexibilität, die Implementierung des Basistyps sowie die im Protokoll definierte zusätzliche Schnittstelle durchführen zu können.

Gibt es einen anderen offensichtlicheren Weg, den ich vermissen könnte?

Beispiel

Angenommen, ich habe eine UITableViewCellFabrik, die für die Rückgabe von Zellen gemäß einem Protokoll verantwortlich ist. Wir können leicht eine generische Funktion einrichten, die Zellen zurückgibt, die einem Protokoll entsprechen:

class CellFactory {
    class func createCellForItem<T: UITableViewCell where T:MyProtocol >(item: SpecialItem,tableView: UITableView) -> T {
        //etc
    }
}

Später möchte ich diese Zellen aus der Warteschlange entfernen und dabei sowohl den Typ als auch das Protokoll nutzen

var cell: MyProtocol = CellFactory.createCellForItem(somethingAtIndexPath) as UITableViewCell

Dies gibt einen Fehler zurück, da eine Tabellenansichtszelle nicht dem Protokoll entspricht ...

Ich möchte angeben können, dass die Zelle a ist UITableViewCellund der MyProtocolin der Variablendeklaration entspricht.

Rechtfertigung

Wenn Sie mit dem Factory-Muster vertraut sind, ist dies sinnvoll, wenn Sie Objekte einer bestimmten Klasse zurückgeben können, die eine bestimmte Schnittstelle implementieren.

Genau wie in meinem Beispiel möchten wir manchmal Schnittstellen definieren, die sinnvoll sind, wenn sie auf ein bestimmtes Objekt angewendet werden. Mein Beispiel für die Zelle der Tabellenansicht ist eine solche Rechtfertigung.

Der angegebene Typ entspricht zwar nicht genau der genannten Schnittstelle, das von der Factory zurückgegebene Objekt jedoch. Daher möchte ich die Flexibilität bei der Interaktion sowohl mit dem Basisklassentyp als auch mit der deklarierten Protokollschnittstelle

Daniel Galasko
quelle
Sorry, aber worum geht es in Kürze? Typen wissen bereits, welche Protokolle sie erfüllen. Was nicht nur den Typ verwenden?
Kirsteins
1
@ Kirsteins Nicht, wenn der Typ nicht von einer Fabrik zurückgegeben wird und somit ein generischer Typ mit einer gemeinsamen Basisklasse ist
Daniel Galasko
Bitte geben Sie wenn möglich ein Beispiel.
Kirsteins
NSSomething<ABCProtocolOne,ABCProtocolTwo> * object = ...;. Dieses Objekt scheint ziemlich nutzlos zu sein, da es NSSomethingbereits weiß, wozu es passt. Wenn es entspricht nicht einem der Protokolle in <>Sie bekommen unrecognised selector ...stürzt ab. Dies bietet überhaupt keine Typensicherheit.
Kirsteins
@ Kirsteins Bitte sehen Sie sich mein Beispiel noch einmal an. Es wird verwendet, wenn Sie wissen, dass das Objekt, das Ihre Fabrik verkauft, einer bestimmten Basisklasse angehört, die einem bestimmten Protokoll entspricht
Daniel Galasko,

Antworten:

72

In Swift 4 ist es jetzt möglich, eine Variable zu deklarieren, die eine Unterklasse eines Typs ist und gleichzeitig ein oder mehrere Protokolle implementiert.

var myVariable: MyClass & MyProtocol & MySecondProtocol

So führen Sie eine optionale Variable aus:

var myVariable: (MyClass & MyProtocol & MySecondProtocol)?

oder als Parameter einer Methode:

func shakeEm(controls: [UIControl & Shakeable]) {}

Apple kündigte dies auf der WWDC 2017 in Sitzung 402 an: Was ist neu in Swift?

Zweitens möchte ich über das Verfassen von Klassen und Protokollen sprechen. Hier habe ich dieses verwackelbare Protokoll für ein UI-Element eingeführt, das einen kleinen Verwacklungseffekt erzeugen kann, um die Aufmerksamkeit auf sich selbst zu lenken. Und ich habe einige der UIKit-Klassen erweitert, um diese Shake-Funktionalität tatsächlich bereitzustellen. Und jetzt möchte ich etwas schreiben, das einfach erscheint. Ich möchte nur eine Funktion schreiben, die eine Reihe von Steuerelementen verwendet, die verwackelt werden können, und diejenigen erschüttert, die aktiviert sind, um die Aufmerksamkeit auf sie zu lenken. Welchen Typ kann ich hier in dieses Array schreiben? Es ist eigentlich frustrierend und knifflig. Ich könnte also versuchen, ein UI-Steuerelement zu verwenden. Aber nicht alle UI-Steuerelemente sind in diesem Spiel verwackelbar. Ich könnte shakable versuchen, aber nicht alle shakables sind UI-Steuerelemente. Und es gibt eigentlich keine gute Möglichkeit, dies in Swift 3 darzustellen.Swift 4 führt den Gedanken ein, eine Klasse mit einer beliebigen Anzahl von Protokollen zu erstellen.

Philipp Otto
quelle
3
Fügen Sie
Danke Philipp!
Omar Albeik
Was ist, wenn eine optionale Variable dieses Typs benötigt wird?
Vyachaslav Gerchicov
2
@VyachaslavGerchicov: Sie können Klammern darum setzen und dann das Fragezeichen wie folgt: var myVariable: (MyClass & MyProtocol & MySecondProtocol)?
Philipp Otto
30

Sie können keine Variable wie deklarieren

var object:Base,protocol<ProtocolOne,ProtocolTwo> = ...

noch deklarieren Funktionstyp wie

func someFunc() -> Base,protocol<MyProtocol,Protocol2> { ... }

Sie können als Funktionsparameter wie diesen deklarieren, aber es ist im Grunde ein Upcasting.

func someFunc<T:Base where T:protocol<MyProtocol1,MyProtocol2>>(val:T) {
    // here, `val` is guaranteed to be `Base` and conforms `MyProtocol` and `MyProtocol2`
}

class SubClass:BaseClass, MyProtocol1, MyProtocol2 {
   //...
}

let val = SubClass()
someFunc(val)

Ab sofort können Sie nur noch Folgendes tun:

class CellFactory {
    class func createCellForItem(item: SpecialItem) -> UITableViewCell {
        return ... // any UITableViewCell subclass
    }
}

let cell = CellFactory.createCellForItem(special)
if let asProtocol = cell as? protocol<MyProtocol1,MyProtocol2> {
    asProtocol.protocolMethod()
    cell.cellMethod()
}

Damit ist technisch cellidentisch mit asProtocol.

Aber was den Compiler betrifft, cellhat er UITableViewCellnur eine Schnittstelle von , während er asProtocolnur eine Protokollschnittstelle hat. Wenn Sie also die UITableViewCellMethoden aufrufen möchten , müssen Sie eine cellVariable verwenden. Wenn Sie die Protokollmethode aufrufen möchten, verwenden Sie asProtocolvariable.

Wenn Sie sicher sind, dass die Zelle den Protokollen entspricht, müssen Sie sie nicht verwenden if let ... as? ... {}. mögen:

let cell = CellFactory.createCellForItem(special)
let asProtocol = cell as protocol<MyProtocol1,MyProtocol2>
Rintaro
quelle
Da die Fabrik die Rückgabetypen spezifiziert, muss ich die optionale Besetzung technisch nicht durchführen? Ich könnte mich einfach auf die implizite Eingabe von Swifts verlassen, um die Eingabe durchzuführen, bei der ich die Protokolle explizit deklariere.
Daniel Galasko
Ich verstehe nicht, was du meinst, entschuldige meine schlechten Englischkenntnisse. Wenn Sie darüber sprechen, -> UITableViewCell<MyProtocol>ist dies ungültig, da UITableViewCelles sich nicht um einen generischen Typ handelt. Ich denke, das wird nicht einmal kompiliert.
Rintaro
Ich beziehe mich nicht auf Ihre generische Implementierung, sondern auf Ihre beispielhafte Darstellung der Implementierung. wo du sagst lass asProtocol = ...
Daniel Galasko
oder ich könnte einfach tun: var cell: protocol <ProtocolOne, ProtocolTwo> = someObject als UITableViewCell und den Vorteil von beiden in einer Variablen erhalten
Daniel Galasko
2
Das glaube ich nicht. Auch wenn Sie so etwas tun könnten, cellgibt es nur Protokollmethoden (für Compiler).
Rintaro
2

Leider unterstützt Swift keine Protokollkonformität auf Objektebene. Es gibt jedoch eine etwas umständliche Problemumgehung, die Ihren Zwecken dienen kann.

struct VCWithSomeProtocol {
    let protocol: SomeProtocol
    let viewController: UIViewController

    init<T: UIViewController>(vc: T) where T: SomeProtocol {
        self.protocol = vc
        self.viewController = vc
    }
}

Überall dort, wo Sie etwas tun müssen, über das UIViewController verfügt, greifen Sie auf den .viewController-Aspekt der Struktur zu, und auf alles, was Sie für den Protokollaspekt benötigen, verweisen Sie auf das .protocol.

Zum Beispiel:

class SomeClass {
   let mySpecialViewController: VCWithSomeProtocol

   init<T: UIViewController>(injectedViewController: T) where T: SomeProtocol {
       self.mySpecialViewController = VCWithSomeProtocol(vc: injectedViewController)
   }
}

Immer wenn Sie mySpecialViewController benötigen, um etwas mit UIViewController zu tun, verweisen Sie einfach auf mySpecialViewController.viewController, und wenn Sie eine Protokollfunktion benötigen, verweisen Sie auf mySpecialViewController.protocol.

Hoffentlich können wir mit Swift 4 in Zukunft ein Objekt mit daran angehängten Protokollen deklarieren. Aber im Moment funktioniert das.

Hoffe das hilft!

Michael Curtis
quelle
1

EDIT: Ich habe mich geirrt , aber wenn jemand anderes dieses Missverständnis wie ich liest, lasse ich diese Antwort da draußen. Das OP fragte nach der Überprüfung der Protokollkonformität des Objekts einer bestimmten Unterklasse, und das ist eine andere Geschichte, wie die akzeptierte Antwort zeigt. Diese Antwort spricht über die Protokollkonformität für die Basisklasse.

Vielleicht irre ich mich, aber reden Sie nicht darüber, der UITableCellViewKlasse Protokollkonformität hinzuzufügen ? Das Protokoll wird in diesem Fall auf die Basisklasse und nicht auf das Objekt erweitert. Weitere Informationen finden Sie in der Dokumentation von Apple zum Deklarieren der Protokollannahme mit einer Erweiterung, die in Ihrem Fall ungefähr so ​​aussehen würde:

extension UITableCellView : ProtocolOne {}

// Or alternatively if you need to add a method, protocolMethod()
extension UITableCellView : ProcotolTwo {
   func protocolTwoMethod() -> String {
     return "Compliant method"
   }
}

Neben der bereits erwähnten Swift-Dokumentation finden Sie im Artikel Allgemeine Funktionen für inkompatible Typen von Nate Cooks weitere Beispiele.

Dies gibt uns die Flexibilität, die Implementierung des Basistyps sowie die im Protokoll definierte zusätzliche Schnittstelle durchführen zu können.

Gibt es einen anderen offensichtlicheren Weg, den ich vermissen könnte?

Die Protokollübernahme führt genau dies aus und bewirkt, dass ein Objekt das angegebene Protokoll einhält. Beachten Sie jedoch der negativen Seite, dass eine Variable eines bestimmten Protokolltyps ist nicht alles wissen , außerhalb des Protokolls. Dies kann jedoch umgangen werden, indem ein Protokoll definiert wird, das alle erforderlichen Methoden / Variablen / ...

Der angegebene Typ entspricht zwar nicht genau der genannten Schnittstelle, das von der Factory zurückgegebene Objekt jedoch. Daher möchte ich die Flexibilität bei der Interaktion sowohl mit dem Basisklassentyp als auch mit der deklarierten Protokollschnittstelle

Wenn Sie eine generische Methode wünschen, deren Variable sowohl einem Protokoll- als auch einem Basisklassentyp entspricht, haben Sie möglicherweise kein Glück. Es hört sich jedoch so an, als müssten Sie das Protokoll breit genug definieren, um über die erforderlichen Konformitätsmethoden zu verfügen, und gleichzeitig eng genug, um es ohne zu viel Arbeit in Basisklassen übernehmen zu können (dh nur zu erklären, dass eine Klasse dem entspricht Protokoll).

holroy
quelle
1
Das ist überhaupt nicht das, worüber ich spreche, aber danke :) Ich wollte in der Lage sein, mit einem Objekt sowohl über seine Klasse als auch über ein bestimmtes Protokoll zu kommunizieren. Genau wie in obj-c kann ich NSObject <MyProtocol> obj = ... Unnötig zu sagen, dass dies nicht schnell erledigt werden kann, müssen Sie das Objekt in sein Protokoll
umwandeln
0

Ich hatte einmal eine ähnliche Situation, als ich versuchte, meine generischen Interaktorverbindungen in Storyboards zu verknüpfen (IB erlaubt Ihnen nicht, Outlets mit Protokollen zu verbinden, sondern nur mit Objektinstanzen) Eigentum. Dies hindert zwar niemanden daran, per se illegale Zuweisungen vorzunehmen, bietet jedoch eine bequeme Möglichkeit, unerwünschte Interaktionen mit einer nicht konformen Instanz zur Laufzeit sicher zu verhindern. (dh verhindern, dass Delegatenmethoden an Objekte aufgerufen werden, die nicht dem Protokoll entsprechen.)

Beispiel:

@objc protocol SomeInteractorInputProtocol {
    func getSomeString()
}

@objc protocol SomeInteractorOutputProtocol {
    optional func receiveSomeString(value:String)
}

@objc class SomeInteractor: NSObject, SomeInteractorInputProtocol {

    @IBOutlet var outputReceiver : AnyObject? = nil

    private var protocolOutputReceiver : SomeInteractorOutputProtocol? {
        get { return self.outputReceiver as? SomeInteractorOutputProtocol }
    }

    func getSomeString() {
        let aString = "This is some string."
        self.protocolOutputReceiver?.receiveSomeString?(aString)
    }
}

Der "outputReceiver" wird als optional deklariert, ebenso wie der private "protocolOutputReceiver". Indem ich immer über diesen (die berechnete Eigenschaft) auf den outputReceiver (auch bekannt als delegate) zugreife, filtere ich effektiv alle Objekte heraus, die nicht dem Protokoll entsprechen. Jetzt kann ich einfach die optionale Verkettung verwenden, um das delegierte Objekt sicher aufzurufen, ob es das Protokoll implementiert oder überhaupt existiert.

Um dies auf Ihre Situation anzuwenden, können Sie den öffentlichen ivar vom Typ "YourBaseClass?" (im Gegensatz zu AnyObject) und verwenden Sie die private berechnete Eigenschaft, um die Protokollkonformität zu erzwingen. FWIW.

Quickthyme
quelle