web-dev-qa-db-fra.com

Utilisation de protocoles en tant que types de tableaux et paramètres de fonction dans swift

Je souhaite créer une classe pouvant stocker des objets conformes à un protocole donné. Les objets doivent être stockés dans un tableau typé. Selon la documentation Swift, les protocoles peuvent être utilisés en tant que types:

Comme il s'agit d'un type, vous pouvez utiliser un protocole dans de nombreux endroits où d'autres types sont autorisés, notamment:

  • En tant que type de paramètre ou type de retour dans une fonction, une méthode ou un initialiseur
  • En tant que type d'une constante, d'une variable ou d'une propriété
  • En tant que type d'éléments dans un tableau, un dictionnaire ou un autre conteneur

Cependant, les erreurs suivantes du compilateur sont générées: 

Le protocole 'SomeProtocol' ne peut être utilisé que comme contrainte générique car il a des exigences de type Self ou associé

Comment êtes-vous censé résoudre ceci:

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

Vous avez rencontré une variante d'un problème de protocoles dans Swift pour lequel aucune bonne solution n'existe encore.

Voir aussi Extension du tableau pour vérifier s'il est trié dans Swift? , il contient des suggestions sur la manière de le contourner qui pourrait convenir à votre problème spécifique (votre question est très générique, vous pouvez peut-être trouver une solution de contournement à l'aide de ces réponses).

41
DarkDust

Vous voulez créer une classe générique, avec une contrainte de type nécessitant que les classes utilisées avec celle-ci soient conformes à SomeProtocol, comme ceci:

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)
        }
    }
}
30
Nate Cook

Dans Swift, il existe une classe spéciale de protocoles qui ne fournit pas de polymorphisme par rapport aux types qui l'implémentent. De tels protocoles utilisent des mots-clés Self ou associatedtype dans leurs définitions (et Equatable en est une).

Dans certains cas, il est possible d'utiliser un wrapper effacé pour rendre votre collection homomorphique. Ci-dessous un exemple.

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

La solution limitée que j'ai trouvée consiste à marquer le protocole en tant que protocole de classe uniquement. Cela vous permettra de comparer des objets en utilisant l'opérateur '==='. Je comprends que cela ne fonctionnera pas pour les structures, etc., mais c’était suffisant dans mon cas.

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

}
8
almas

La solution est assez simple:

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

Je suppose que votre objectif principal est de conserver une collection d’objets conformes à un protocole, d’ajouter à cette collection et de la supprimer. C’est la fonctionnalité indiquée dans votre client, "SomeClass". Un héritage équitable requiert soi et ce n'est pas nécessaire pour cette fonctionnalité. Nous aurions pu faire fonctionner ce travail dans des tableaux dans Obj-C en utilisant la fonction "index" pouvant prendre un comparateur personnalisé, mais cela n'est pas supporté dans Swift. La solution la plus simple consiste donc à utiliser un dictionnaire au lieu d'un tableau, comme indiqué dans le code ci-dessous. J'ai fourni getElements () qui vous rendra le tableau de protocoles que vous vouliez. Ainsi, quiconque utilisant SomeClass ne saurait même pas qu'un dictionnaire a été utilisé pour la mise en œuvre.

Puisque dans tous les cas, vous auriez besoin de propriétés distinctives pour séparer vos objets, j'ai supposé que c'était "nom". Assurez-vous que votre do element.name = "foo" lorsque vous créez une nouvelle instance SomeProtocol. Si le nom n'est pas défini, vous pouvez toujours créer l'instance, mais celle-ci ne sera pas ajoutée à la collection et addElement () renverra "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)
    }
}
2
Jitendra Kulkarni

J'ai trouvé une solution Swift pure/pure pas pure sur cet article de blog: http://blog.inferis.org/blog/2015/05/27/Swift-an-array-of-protocols/

L'astuce consiste à se conformer à NSObjectProtocol car il introduit isEqual(). Par conséquent, au lieu d'utiliser le protocole Equatable et son utilisation par défaut de ==, vous pouvez écrire votre propre fonction pour rechercher l'élément et le supprimer.

Voici l'implémentation de votre fonction find(array, element) -> Int?:

protocol SomeProtocol: NSObjectProtocol {

}

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

    return nil
}

Remarque: Dans ce cas, vos objets conformes à SomeProtocol doivent hériter de NSObject.

0
Kevin Delord