Utilisation de protocoles comme types de tableaux et paramètres de fonction dans Swift

137

Je veux créer une classe qui peut stocker des objets conformes à un certain protocole. Les objets doivent être stockés dans un tableau typé. Selon la documentation Swift, les protocoles peuvent être utilisés comme 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, ce qui suit génère des erreurs de compilation:

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)
        }
    }
}
snod
la source
2
Dans Swift, il existe une classe spéciale de protocoles qui ne fournit pas de polymorphisme sur les types qui l'implémentent. De tels protocoles utilisent Self ou associatedtype dans sa définition (et Equatable est l'un d'entre eux). Dans certains cas, il est possible d'utiliser un wrapper effacé pour rendre votre collection homomorphe. Regardez ici par exemple.
werediver

Réponses:

48

Vous avez rencontré une variante d'un problème avec les 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 façon de contourner ce problème qui peuvent convenir à votre problème spécifique (votre question est très générique, vous pouvez peut-être trouver une solution de contournement en utilisant ces réponses).

DarkDust
la source
1
Je pense que c'est la bonne réponse pour le moment. La solution de Nate fonctionne mais ne résout pas entièrement mon problème.
snod le
32

Vous voulez créer une classe générique, avec une contrainte de type qui nécessite que les classes utilisées avec elle 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)
        }
    }
}
Nate Cook
la source
Comment instancieriez-vous un objet de cette classe?
snod le
Hmmm ... De cette façon, vous utilisez un seul type conforme à SomeProtocol-let protocolGroup: SomeClass<MyMemberClass> = SomeClass()
Nate Cook
De cette façon, vous ne pouviez ajouter que des objets de classe MyMemberClassau tableau?
snod le
oulet foo = SomeClass<MyMemberClass>()
DarkDust
@snod Ouais, ce n'est pas ce que vous recherchez. Le problème est la Equatableconformité - sans cela, vous pouvez utiliser votre code exact. Peut-être déposer une demande de bogue / fonctionnalité?
Nate Cook
15

Dans Swift, il existe une classe spéciale de protocoles qui ne fournit pas de polymorphisme sur les types qui l'implémentent. Ces protocoles utilisent des mots Self- associatedtypeclés ou dans leurs définitions (et Equatableest l'un d'entre eux).

Dans certains cas, il est possible d'utiliser un wrapper effacé pour rendre votre collection homomorphe. Voici 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
werediver
la source
12

La solution limitée que j'ai trouvée est de marquer le protocole comme un 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 assez bon 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
            }
        }
    }

}
almas
la source
Cela n'autorise-t-il pas les entrées en double dans protocols, si addElementest appelé plus d'une fois avec le même objet?
Tom Harrington
Oui, les tableaux dans swift peuvent contenir des entrées en double. Si vous pensez que cela peut se produire dans votre code, utilisez le Set au lieu du tableau ou assurez-vous que ce tableau ne contient pas déjà cet objet.
almas
Vous pouvez appeler removeElement()avant d'ajouter le nouvel élément si vous souhaitez éviter les doublons.
Georgios
Je veux dire comment vous contrôlez votre tableau est dans les airs, non? Merci pour la réponse
Reimond Hill
9

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
        }
    }
}
bzz
la source
4
Vous avez manqué l'important: l'OP veut que le protocole hérite du Equatableprotocole. Cela fait une énorme différence.
werediver
@werediver Je ne pense pas. Il souhaite stocker des objets conformes à SomeProtocoldans un tableau typé. Equatablela conformité n'est requise que pour supprimer des éléments du tableau. Ma solution est une version améliorée de la solution @almas car elle peut être utilisée avec n'importe quel type Swift conforme au Equatableprotocole.
bzz le
2

Je suppose que votre objectif principal est de conserver une collection d'objets conformes à un protocole, d'ajouter à cette collection et de la supprimer. Il s'agit de la fonctionnalité indiquée dans votre client, "SomeClass". L'héritage équatable nécessite self et cela n'est pas nécessaire pour cette fonctionnalité. Nous aurions pu faire ce travail dans des tableaux dans Obj-C en utilisant la fonction "index" qui peut prendre un comparateur personnalisé mais cela n'est pas pris en charge 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 l'implémentation.

Puisque dans tous les cas, vous auriez besoin d'une propriété distinctive pour séparer vos objets, j'ai supposé que c'était "nom". Veuillez vous assurer que votre do element.name = "foo" lorsque vous créez une nouvelle instance de SomeProtocol. Si le nom n'est pas défini, vous pouvez toujours créer l'instance, mais elle 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)
    }
}
Jitendra Kulkarni
la source
0

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

L'astuce est de se conformer à NSObjectProtocolce qu'il introduit isEqual(). Par conséquent, au lieu d'utiliser le Equatableprotocole et son utilisation par défaut, ==vous pouvez écrire votre propre fonction pour trouver l'élément et le supprimer.

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

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 à SomeProtocoldoivent hériter de NSObject.

Kevin Delord
la source