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

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


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

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

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

}

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

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

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.

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.