Utilizzo di protocolli come tipi di array e parametri di funzione in tempi rapidi


137

Voglio creare una classe in grado di memorizzare oggetti conformi a un determinato protocollo. Gli oggetti devono essere archiviati in un array tipizzato. Secondo i protocolli di documentazione Swift possono essere utilizzati come tipi: 

Poiché è un tipo, è possibile utilizzare un protocollo in molti luoghi in cui sono consentiti altri tipi, tra cui:

  • Come tipo di parametro o tipo restituito in una funzione, metodo o inizializzatore
  • Come il tipo di una costante, variabile o proprietà
  • Come il tipo di elementi in un array, dizionario o altro contenitore

Tuttavia, quanto segue genera errori del compilatore:

Il protocollo "SomeProtocol" può essere utilizzato solo come vincolo generico perché ha requisiti di tipo Self o associati

Come dovresti risolvere questo:

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
In Swift esiste una classe speciale di protocolli che non fornisce polimorfismo rispetto ai tipi che lo implementano. Tali protocolli usano Self o il tipo associato nella sua definizione (ed Equatable è uno di questi). In alcuni casi è possibile utilizzare un wrapper cancellato dal tipo per rendere omogenea la tua raccolta. Guarda qui per esempio.
Werediver,

Risposte:


48

Hai riscontrato una variante di un problema con i protocolli di Swift per i quali non esiste ancora una buona soluzione.

Vedi anche Estensione dell'array per verificare se è ordinato in Swift? , contiene suggerimenti su come aggirare il problema che potrebbero essere adatti al tuo problema specifico (la tua domanda è molto generica, forse puoi trovare una soluzione alternativa usando queste risposte).


1
Penso che questa sia la risposta corretta per il momento. La soluzione di Nate funziona ma non risolve completamente il mio problema.
snod

32

Si desidera creare una classe generica, con un vincolo di tipo che richiede che le classi utilizzate con essa siano conformi SomeProtocol, in questo modo:

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

Come istanzeresti un oggetto di quella classe?
snod

Hmmm ... In questo modo ti blocca usando un solo tipo conforme a SomeProtocol-let protocolGroup: SomeClass<MyMemberClass> = SomeClass()
Nate Cook,

In questo modo potresti aggiungere solo oggetti di classe MyMemberClassall'array?
snod

oppurelet foo = SomeClass<MyMemberClass>()
DarkDust,

@snod Sì, che non è quello che stai cercando. Il problema è la Equatableconformità - senza che è possibile utilizzare il codice esatto. Forse presentare una richiesta di bug / funzionalità?
Nate Cook,

15

In Swift esiste una classe speciale di protocolli che non fornisce polimorfismo rispetto ai tipi che lo implementano. Tali protocolli utilizzano Selfo associatedtypeparole chiave nelle loro definizioni (ed Equatableè uno di questi).

In alcuni casi è possibile utilizzare un wrapper cancellato per scrivere la tua collezione omomorfa. Di seguito è riportato un esempio.

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

La soluzione limitata che ho trovato è quella di contrassegnare il protocollo come protocollo solo di classe. Questo ti permetterà di confrontare gli oggetti usando l'operatore '==='. Capisco che questo non funzionerà per le strutture, ecc., Ma è stato abbastanza buono nel mio caso.

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

}

Questo non consente voci duplicate in protocols, se addElementviene chiamato più di una volta con lo stesso oggetto?
Tom Harrington,

Sì, gli array in swift possono contenere voci duplicate. Se ritieni che ciò possa accadere nel tuo codice, utilizza sia Set che array o assicurati che l'array non contenga già quell'oggetto.
almas,

È possibile chiamare removeElement()prima di aggiungere il nuovo elemento se si desidera evitare duplicati.
Georgios,

Voglio dire come controlli il tuo array in aria, giusto? Grazie per la risposta
Reimond Hill,

9

La soluzione è piuttosto semplice:

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
Ti sei perso l'importante: l'OP vuole che il protocollo erediti il Equatableprotocollo. Fa un'enorme differenza.
Werediver,

@werediver Non la penso così. Vuole archiviare oggetti conformi a SomeProtocolin una matrice tipizzata. Equatablela conformità è richiesta solo per la rimozione di elementi dall'array. La mia soluzione è una versione migliorata della soluzione @almas perché può essere utilizzata con qualsiasi tipo di Swift conforme al Equatableprotocollo.
bzz,

2

Ritengo che il tuo obiettivo principale sia quello di contenere una raccolta di oggetti conforme ad alcuni protocolli, aggiungere a questa raccolta ed eliminare da essa. Questa è la funzionalità indicata nel client "SomeClass". L'eredità equa richiede sé e ciò non è necessario per questa funzionalità. Avremmo potuto fare questo lavoro in array in Obj-C usando la funzione "index" che può prendere un comparatore personalizzato ma questo non è supportato in Swift. Quindi la soluzione più semplice è usare un dizionario invece di un array come mostrato nel codice qui sotto. Ho fornito getElements () che ti restituirà l'array di protocollo desiderato. Quindi chiunque usi SomeClass non saprebbe nemmeno che un dizionario è stato usato per l'implementazione.

Dato che, in ogni caso, avresti bisogno di alcune proprietà distintive per separare i tuoi oggetti, ho assunto che si tratti di "nome". Assicurati che do element.name = "pippo" quando crei una nuova istanza SomeProtocol. Se il nome non è impostato, è comunque possibile creare l'istanza, ma non verrà aggiunto alla raccolta e addElement () restituirà "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

Ho trovato una soluzione Swift non pura in quel post sul blog: http://blog.inferis.org/blog/2015/05/27/swift-an-array-of-protocols/

Il trucco è conformarsi a NSObjectProtocolcome introduce isEqual(). Pertanto, invece di utilizzare il Equatableprotocollo e il suo utilizzo predefinito, ==è possibile scrivere la propria funzione per trovare l'elemento e rimuoverlo.

Ecco l'implementazione della tua find(array, element) -> Int?funzione:

protocol SomeProtocol: NSObjectProtocol {

}

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

    return nil
}

Nota: in questo caso gli oggetti conformi a SomeProtocoldevono ereditare da NSObject.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.