Wykorzystanie protokołów jako typów tablic i parametrów funkcji w języku swift

136

Chcę utworzyć klasę, która może przechowywać obiekty zgodne z określonym protokołem. Obiekty powinny być przechowywane w tablicy typu. Zgodnie z dokumentacją Swift protokoły mogą być używane jako typy: 

Ponieważ jest to typ, możesz używać protokołu w wielu miejscach, w których dozwolone są inne typy, w tym:

  • Jako typ parametru lub typ zwracany w funkcji, metodzie lub inicjatorze
  • Jako typ stałej, zmiennej lub właściwości
  • Jako typ elementów w tablicy, słowniku lub innym kontenerze

Jednak następujące generuje błędy kompilatora:

Protokół „SomeProtocol” może być używany tylko jako ograniczenie ogólne, ponieważ ma wymagania typu Self lub powiązane

Jak masz to rozwiązać:

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
źródło
2
W Swift istnieje specjalna klasa protokołów, która nie zapewnia polimorfizmu w stosunku do typów, które go implementują. Takie protokoły używają w swojej definicji typu Self lub powiązanego typu (a Equatable jest jednym z nich). W niektórych przypadkach można użyć opakowania z wymazaniem tekstu, aby nadać kolekcji homomorfizm. Spójrz tutaj na przykład.
werediver

Odpowiedzi:

48

Trafiłeś w wariant problemu z protokołami w Swift, dla którego nie istnieje jeszcze dobre rozwiązanie.

Zobacz także Rozszerzanie tablicy, aby sprawdzić, czy jest posortowana w języku Swift? , zawiera sugestie, jak obejść ten problem, co może być odpowiednie dla konkretnego problemu (Twoje pytanie jest bardzo ogólne, być może możesz znaleźć obejście, korzystając z tych odpowiedzi).

DarkDust
źródło
1
Myślę, że na razie to jest poprawna odpowiedź. Rozwiązanie Nate'a działa, ale nie rozwiązuje całkowicie mojego problemu.
snod
32

Chcesz utworzyć klasę ogólną z ograniczeniem typu, które wymaga, aby klasy używane z nią były zgodne SomeProtocol, na przykład:

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
źródło
Jak utworzyłbyś instancję obiektu tej klasy?
snod
Hmmm ... W ten sposób uzależniasz się od używania jednego typu, który jest zgodny z SomeProtocol-let protocolGroup: SomeClass<MyMemberClass> = SomeClass()
Nate Cook.
W ten sposób mogłeś tylko dodawać obiekty klasy MyMemberClassdo tablicy?
snod
lublet foo = SomeClass<MyMemberClass>()
DarkDust
@snod Tak, a to nie jest to, czego szukasz. Problemem jest Equatablezgodność - bez tego możesz użyć dokładnego kodu. Może zgłoś błąd / prośbę o funkcję?
Nate Cook
15

W Swift istnieje specjalna klasa protokołów, która nie zapewnia polimorfizmu w stosunku do typów, które go implementują. Takie protokoły używają Selflub associatedtypesłów kluczowych w swoich definicjach (i Equatablejest jednym z nich).

W niektórych przypadkach można użyć opakowania z wymazaniem tekstu, aby nadać kolekcji homomorfizm. Poniżej przykład.

// 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
źródło
12

Ograniczone rozwiązanie, które znalazłem, polega na oznaczeniu protokołu jako protokołu tylko dla klasy. Umożliwi to porównywanie obiektów za pomocą operatora „===”. Rozumiem, że to nie zadziała w przypadku struktur itp., Ale w moim przypadku było wystarczająco dobre.

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

}
almy
źródło
Czy to nie zezwala na zduplikowane wpisy w protocols, jeśli addElementjest wywoływane więcej niż raz z tym samym obiektem?
Tom Harrington
Tak, tablice w swift mogą zawierać zduplikowane wpisy. Jeśli uważasz, że może się to zdarzyć w twoim kodzie, użyj Set zamiast tablicy lub upewnij się, że tablica nie zawiera już tego obiektu.
almas
Możesz zadzwonić removeElement()przed dołączeniem nowego elementu, jeśli chcesz uniknąć duplikatów.
Georgios
Mam na myśli to, jak kontrolujesz swój zestaw w powietrzu, prawda? Dziękuję za odpowiedź
Reimond Hill
9

Rozwiązanie jest dość proste:

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
źródło
4
Przegapiłeś ważną rzecz: OP chce, aby Equatableprotokół dziedziczył protokół. To robi ogromną różnicę.
werediver
@ Odpowiednik, nie sądzę. Chce przechowywać obiekty zgodne z SomeProtocoltypem tablicy. Equatablezgodność jest wymagana tylko do usuwania elementów z tablicy. Moje rozwiązanie jest ulepszoną wersją rozwiązania @almas, ponieważ może być używane z dowolnym typem Swift, który jest zgodny z Equatableprotokołem.
bzz
2

Rozumiem, że twoim głównym celem jest przechowywanie kolekcji obiektów zgodnych z jakimś protokołem, dodawanie do tej kolekcji i usuwanie z niej. To jest funkcja określona w kliencie „SomeClass”. Dziedziczenie równoważne wymaga siebie, a to nie jest potrzebne do tej funkcji. Mogliśmy to zrobić w tablicach w Obj-C przy użyciu funkcji „index”, która może pobierać niestandardowy komparator, ale nie jest to obsługiwane w języku Swift. Zatem najprostszym rozwiązaniem jest użycie słownika zamiast tablicy, jak pokazano w kodzie poniżej. Udostępniłem metodę getElements (), która zwróci żądaną tablicę protokołów. Więc każdy, kto używa SomeClass, nie wiedziałby nawet, że do implementacji użyto słownika.

Ponieważ w każdym razie potrzebowałbyś jakiejś wyróżniającej właściwości, aby oddzielić swoje obiekty, założyłem, że jest to „nazwa”. Upewnij się, że do element.name = "foo" podczas tworzenia nowej instancji SomeProtocol. Jeśli nazwa nie jest ustawiona, nadal możesz utworzyć instancję, ale nie zostanie ona dodana do kolekcji, a metoda addElement () zwróci wartość „false”.

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
źródło
0

Znalazłem nie czysto rozwiązanie Swift w tym poście na blogu: http://blog.inferis.org/blog/2015/05/27/swift-an-array-of-protocols/

Sztuczka polega na dostosowaniu się do tego, NSObjectProtocolco wprowadza isEqual(). Dlatego zamiast używać Equatableprotokołu i jego domyślnego użycia, ==możesz napisać własną funkcję, aby znaleźć element i go usunąć.

Oto implementacja Twojej find(array, element) -> Int?funkcji:

protocol SomeProtocol: NSObjectProtocol {

}

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

    return nil
}

Uwaga: w tym przypadku obiekty zgodne z SomeProtocolmust dziedziczą z NSObject.

Kevin Delord
źródło