Verwendung von Protokollen als Array-Typen und Funktionsparameter in kurzer Zeit

136

Ich möchte eine Klasse erstellen, die Objekte speichern kann, die einem bestimmten Protokoll entsprechen. Die Objekte sollten in einem typisierten Array gespeichert werden. Gemäß der Swift-Dokumentation können Protokolle als Typen verwendet werden: 

Da es sich um einen Typ handelt, können Sie ein Protokoll an vielen Stellen verwenden, an denen andere Typen zulässig sind, einschließlich:

  • Als Parametertyp oder Rückgabetyp in einer Funktion, Methode oder einem Initialisierer
  • Als Typ einer Konstante, Variablen oder Eigenschaft
  • Als Elementtyp in einem Array, Wörterbuch oder einem anderen Container

Folgendes erzeugt jedoch Compilerfehler:

Das Protokoll 'SomeProtocol' kann nur als generische Einschränkung verwendet werden, da es Self- oder zugehörige Typanforderungen hat

Wie soll man das lösen:

protocol SomeProtocol: Equatable {
    func bla()
}

class SomeClass {
    
    var protocols = [SomeProtocol]()
    
    func addElement(element: SomeProtocol) {
        self.protocols.append(element)
    }
    
    func removeElement(element: SomeProtocol) {
        if let index = find(self.protocols, element) {
            self.protocols.removeAtIndex(index)
        }
    }
}
snod
quelle
2
In Swift gibt es eine spezielle Klasse von Protokollen, die keinen Polymorphismus gegenüber den Typen bieten, die sie implementieren. Solche Protokolle verwenden in ihrer Definition Self oder den zugehörigen Typ (und Equatable ist einer von ihnen). In einigen Fällen ist es möglich, einen vom Typ gelöschten Wrapper zu verwenden, um Ihre Sammlung homomorph zu machen. Schauen Sie hier zum Beispiel.
Werediver

Antworten:

48

Sie haben eine Variante eines Problems mit Protokollen in Swift gefunden, für die es noch keine gute Lösung gibt.

Siehe auch Array erweitern, um zu überprüfen, ob es in Swift? Sortiert ist. Es enthält Vorschläge zur Umgehung des Problems, die für Ihr spezifisches Problem geeignet sein können (Ihre Frage ist sehr allgemein gehalten. Vielleicht können Sie anhand dieser Antworten eine Problemumgehung finden).

DarkDust
quelle
1
Ich denke, das ist die richtige Antwort für den Moment. Nates Lösung funktioniert, löst mein Problem jedoch nicht vollständig.
Snod
32

Sie möchten eine generische Klasse mit einer Typeinschränkung erstellen, für die die verwendeten Klassen den folgenden Anforderungen entsprechen SomeProtocol:

class SomeClass<T: SomeProtocol> {
    typealias ElementType = T
    var protocols = [ElementType]()

    func addElement(element: ElementType) {
        self.protocols.append(element)
    }

    func removeElement(element: ElementType) {
        if let index = find(self.protocols, element) {
            self.protocols.removeAtIndex(index)
        }
    }
}
Nate Cook
quelle
Wie würden Sie ein Objekt dieser Klasse instanziieren?
Snod
Hmmm ... Auf diese Weise können Sie einen einzelnen Typ verwenden, der SomeProtocol-let protocolGroup: SomeClass<MyMemberClass> = SomeClass()
Nate Cook vom
Auf diese Weise konnten Sie MyMemberClassdem Array nur Objekte der Klasse hinzufügen ?
Snod
oderlet foo = SomeClass<MyMemberClass>()
DarkDust
@snod Ja, das ist nicht das, wonach du suchst. Das Problem ist die EquatableKonformität - ohne diese können Sie Ihren genauen Code verwenden. Möglicherweise eine Fehler- / Funktionsanfrage einreichen?
Nate Cook
15

In Swift gibt es eine spezielle Klasse von Protokollen, die keinen Polymorphismus gegenüber den Typen bieten, die sie implementieren. Solche Protokolle verwenden Selfoder associatedtypeSchlüsselwörter in ihren Definitionen (und Equatablesind eines davon).

In einigen Fällen ist es möglich, einen vom Typ gelöschten Wrapper zu verwenden, um Ihre Sammlung homomorph zu machen. Unten ist ein Beispiel.

// This protocol doesn't provide polymorphism over the types which implement it.
protocol X: Equatable {
    var x: Int { get }
}

// We can't use such protocols as types, only as generic-constraints.
func ==<T: X>(a: T, b: T) -> Bool {
    return a.x == b.x
}

// A type-erased wrapper can help overcome this limitation in some cases.
struct AnyX {
    private let _x: () -> Int
    var x: Int { return _x() }

    init<T: X>(_ some: T) {
        _x = { some.x }
    }
}

// Usage Example

struct XY: X {
    var x: Int
    var y: Int
}

struct XZ: X {
    var x: Int
    var z: Int
}

let xy = XY(x: 1, y: 2)
let xz = XZ(x: 3, z: 4)

//let xs = [xy, xz] // error
let xs = [AnyX(xy), AnyX(xz)]
xs.forEach { print($0.x) } // 1 3
werediver
quelle
12

Die begrenzte Lösung, die ich gefunden habe, besteht darin, das Protokoll als Nur-Klasse-Protokoll zu markieren. Auf diese Weise können Sie Objekte mit dem Operator '===' vergleichen. Ich verstehe, dass dies für Strukturen usw. nicht funktioniert, aber es war in meinem Fall gut genug.

protocol SomeProtocol: class {
    func bla()
}

class SomeClass {

    var protocols = [SomeProtocol]()

    func addElement(element: SomeProtocol) {
        self.protocols.append(element)
    }

    func removeElement(element: SomeProtocol) {
        for i in 0...protocols.count {
            if protocols[i] === element {
                protocols.removeAtIndex(i)
                return
            }
        }
    }

}
Almas
quelle
Erlaubt dies nicht doppelte Einträge in protocols, wenn addElementmehr als einmal mit demselben Objekt aufgerufen wird?
Tom Harrington
Ja, Arrays in Swift können doppelte Einträge enthalten. Wenn Sie der Meinung sind, dass dies in Ihrem Code passieren kann, verwenden Sie entweder Set anstelle von Array oder stellen Sie sicher, dass das Array dieses Objekt nicht bereits enthält.
Almas
Sie können removeElement()vor dem Anhängen des neuen Elements aufrufen, wenn Sie Duplikate vermeiden möchten.
Georgios
Ich meine, wie Sie Ihr Array steuern, liegt in der Luft, oder? Vielen Dank für die Antwort
Reimond Hill
9

Die Lösung ist ziemlich einfach:

protocol SomeProtocol {
    func bla()
}

class SomeClass {
    init() {}

    var protocols = [SomeProtocol]()

    func addElement<T: SomeProtocol where T: Equatable>(element: T) {
        protocols.append(element)
    }

    func removeElement<T: SomeProtocol where T: Equatable>(element: T) {
        protocols = protocols.filter {
            if let e = $0 as? T where e == element {
                return false
            }
            return true
        }
    }
}
bzz
quelle
4
Sie haben das Wichtige verpasst: Das OP möchte, dass das Protokoll das Protokoll erbt Equatable. Es macht einen großen Unterschied.
Werediver
@werediver Das glaube ich nicht. Er möchte Objekte, die den Anforderungen entsprechen, SomeProtocolin einem typisierten Array speichern . EquatableKonformität ist nur zum Entfernen von Elementen aus dem Array erforderlich. Meine Lösung ist eine verbesserte Version der @ almas-Lösung, da sie mit jedem Swift-Typ verwendet werden kann, der dem EquatableProtokoll entspricht .
bzz
2

Ich gehe davon aus, dass Ihr Hauptziel darin besteht, eine Sammlung von Objekten zu halten, die einem Protokoll entsprechen, diese Sammlung zu ergänzen und daraus zu löschen. Dies ist die in Ihrem Client angegebene Funktion "SomeClass". Gleichwertige Vererbung erfordert Selbst und das wird für diese Funktionalität nicht benötigt. Wir hätten diese Funktion in Arrays in Obj-C mithilfe der "Index" -Funktion ausführen können, die einen benutzerdefinierten Komparator verwenden kann, dies wird jedoch in Swift nicht unterstützt. Die einfachste Lösung besteht also darin, ein Wörterbuch anstelle eines Arrays zu verwenden, wie im folgenden Code gezeigt. Ich habe getElements () bereitgestellt, mit dem Sie das gewünschte Protokollarray zurückgeben können. Jeder, der SomeClass verwendet, würde also nicht einmal wissen, dass ein Wörterbuch für die Implementierung verwendet wurde.

Da Sie in jedem Fall eine unterscheidende Eigenschaft benötigen würden, um Ihre Objekte zu trennen, habe ich angenommen, dass es sich um "Name" handelt. Stellen Sie sicher, dass do element.name = "foo" ist, wenn Sie eine neue SomeProtocol-Instanz erstellen. Wenn der Name nicht festgelegt ist, können Sie die Instanz weiterhin erstellen, sie wird jedoch nicht zur Auflistung hinzugefügt, und addElement () gibt "false" zurück.

protocol SomeProtocol {
    var name:String? {get set} // Since elements need to distinguished, 
    //we will assume it is by name in this example.
    func bla()
}

class SomeClass {

    //var protocols = [SomeProtocol]() //find is not supported in 2.0, indexOf if
     // There is an Obj-C function index, that find element using custom comparator such as the one below, not available in Swift
    /*
    static func compareProtocols(one:SomeProtocol, toTheOther:SomeProtocol)->Bool {
        if (one.name == nil) {return false}
        if(toTheOther.name == nil) {return false}
        if(one.name ==  toTheOther.name!) {return true}
        return false
    }
   */

    //The best choice here is to use dictionary
    var protocols = [String:SomeProtocol]()


    func addElement(element: SomeProtocol) -> Bool {
        //self.protocols.append(element)
        if let index = element.name {
            protocols[index] = element
            return true
        }
        return false
    }

    func removeElement(element: SomeProtocol) {
        //if let index = find(self.protocols, element) { // find not suported in Swift 2.0


        if let index = element.name {
            protocols.removeValueForKey(index)
        }
    }

    func getElements() -> [SomeProtocol] {
        return Array(protocols.values)
    }
}
Jitendra Kulkarni
quelle
0

Ich habe in diesem Blog-Beitrag eine nicht reine Swift-Lösung gefunden: http://blog.inferis.org/blog/2015/05/27/swift-an-array-of-protocols/

Der Trick besteht darin, sich an NSObjectProtocoldie Einführung anzupassen isEqual(). Anstatt das EquatableProtokoll und seine Standardverwendung zu verwenden, können ==Sie daher eine eigene Funktion schreiben, um das Element zu finden und es zu entfernen.

Hier ist die Implementierung Ihrer find(array, element) -> Int?Funktion:

protocol SomeProtocol: NSObjectProtocol {

}

func find(protocols: [SomeProtocol], element: SomeProtocol) -> Int? {
    for (index, object) in protocols.enumerated() {
        if (object.isEqual(element)) {
            return index
        }
    }

    return nil
}

Hinweis: In diesem Fall müssen Ihre konformen Objekte von SomeProtocolerben NSObject.

Kevin Delord
quelle