web-dev-qa-db-fra.com

Tableaux de génériques en Swift

J'ai joué avec des tableaux de classes génériques avec différents types. Il est plus facile d'expliquer mon problème avec un exemple de code:

// Obviously a very pointless protocol...
protocol MyProtocol {
    var value: Self { get }
}

extension Int   : MyProtocol {  var value: Int    { return self } }
extension Double: MyProtocol {  var value: Double { return self } }

class Container<T: MyProtocol> {
    var values: [T]

    init(_ values: T...) {
        self.values = values
    }

    func myMethod() -> [T] {
        return values
    }
}

Maintenant, si j'essaie de créer un tableau de conteneurs comme ceci:

var containers: [Container<MyProtocol>] = []

Je reçois l'erreur:

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

Pour résoudre ce problème, je peux utiliser [AnyObject]:

let containers: [AnyObject] = [Container<Int>(1, 2, 3), Container<Double>(1.0, 2.0, 3.0)]
// Explicitly stating the types just for clarity.

Mais maintenant, un autre "problème" émerge lors de l'énumération via containers:

for container in containers {
    if let c = container as? Container<Int> {
        println(c.myMethod())

    } else if let c = container as? Container<Double> {
        println(c.myMethod())
    }
}

Comme vous pouvez le voir dans le code ci-dessus, après avoir déterminé le type de container, la même méthode est appelée dans les deux cas. Ma question est:

Existe-t-il un meilleur moyen d'obtenir le Container avec le type correct que de transtyper en tous les types possibles de Container? Ou y a-t-il autre chose que j'ai négligé?

51
ABakerSmith

Il y a un moyen - en quelque sorte - de faire ce que vous voulez - en quelque sorte. Il existe un moyen, avec les protocoles, d'éliminer la restriction de type et d'obtenir toujours le résultat que vous voulez, en quelque sorte, mais ce n'est pas toujours joli. Voici ce que j'ai trouvé comme protocole dans votre situation:

protocol MyProtocol {
    func getValue() -> Self 
}

extension Int: MyProtocol {
    func getValue() -> Int {
        return self
    }
}

extension Double: MyProtocol {
    func getValue() -> Double {
        return self
    }
}

Notez que la propriété value que vous avez initialement mise dans votre déclaration de protocole a été remplacée par une méthode qui renvoie l'objet.

Ce n'est pas très intéressant.

Mais maintenant, comme vous vous êtes débarrassé de la propriété value dans le protocole, MyProtocol peut être utilisé comme type, pas seulement comme contrainte de type. Votre classe Container n'a même plus besoin d'être générique. Vous pouvez le déclarer comme ceci:

class Container {
    var values: [MyProtocol]

    init(_ values: MyProtocol...) {
        self.values = values
    }

    func myMethod() -> [MyProtocol] {
        return values
    }
}

Et comme Container n'est plus générique, vous pouvez créer un Array de Containers et les parcourir, en imprimant les résultats de la méthode myMethod():

var containers = [Container]()

containers.append(Container(1, 4, 6, 2, 6))
containers.append(Container(1.2, 3.5))

for container in containers {
    println(container.myMethod())
}

//  Output: [1, 4, 6, 2, 6]
//          [1.2, 3.5]

L'astuce consiste à construire un protocole qui n'inclut que des fonctions génériques et ne place aucune autre exigence sur un type conforme. Si vous pouvez vous en sortir, vous pouvez utiliser le protocole comme type, et non tout comme une contrainte de type.

Et en prime (si vous voulez l'appeler ainsi), votre tableau de valeurs MyProtocol peut même mélanger différents types conformes à MyProtocol. Donc, si vous donnez à String une extension MyProtocol comme ceci:

extension String: MyProtocol {
    func getValue() -> String {
        return self
    }
}

Vous pouvez réellement initialiser un Container avec des types mixtes:

let container = Container(1, 4.2, "no kidding, this works")

[Avertissement - Je teste cela dans l'une des aires de jeux en ligne. Je n'ai pas encore pu le tester dans Xcode ...]

Modifier:

Si vous voulez toujours que Container soit générique et ne contienne qu'un seul type d'objet, vous pouvez accomplir cela en rendant it conforme à son propre protocole:

protocol ContainerProtocol {
    func myMethod() -> [MyProtocol]
}

class Container<T: MyProtocol>: ContainerProtocol {
    var values: [T] = []

    init(_ values: T...) {
        self.values = values
    } 

    func myMethod() -> [MyProtocol] {
        return values.map { $0 as MyProtocol }
    }
}

Maintenant, vous pouvez encore avoir un tableau d'objets [ContainerProtocol] Et les parcourir en invoquant myMethod():

let containers: [ContainerProtocol] = [Container(5, 3, 7), Container(1.2, 4,5)]

for container in containers {
    println(container.myMethod())
}

Peut-être que cela ne fonctionne toujours pas pour vous, mais maintenant Container est limité à un seul type, et pourtant vous pouvez toujours parcourir un tableau d'objets ContainterProtocol.

48
Aaron Rasmussen

Ceci est un bon exemple de "qu'est-ce que vous avez voulez se produire?" Et démontre en fait la complexité qui explose si Swift avait vraiment des types de première classe.

protocol MyProtocol {
    var value: Self { get }
}

Génial. MyProtocol.value retourne le type qui l'implémente, en se rappelant que cela doit être déterminé au moment de la compilation, pas à l'exécution.

var containers: [Container<MyProtocol>] = []

Donc, déterminé au moment de la compilation, de quel type s'agit-il? Oubliez le compilateur, faites-le simplement sur papier. Ouais, je ne sais pas quel type ce serait. Je veux dire concret type. Pas de métatypes.

let containers: [AnyObject] = [Container<Int>(1, 2, 3), Container<Double>(1.0, 2.0, 3.0)]

Vous savez que vous vous trompez de route lorsque AnyObject s'est glissé dans vos signatures. Rien à ce sujet ne fonctionnera jamais. Après AnyObject est seulement un sac.

Ou y a-t-il autre chose que j'ai oublié?

Oui. Vous avez besoin d'un type et vous n'en avez pas fourni. Vous avez fourni une règle pour contraindre un type, mais aucun type réel. Revenez à votre vrai problème et réfléchissez-y plus en profondeur. (L'analyse de métatype n'est presque jamais votre "vrai" problème, sauf si vous travaillez sur un doctorat CS, auquel cas vous le feriez dans Idris, pas Swift.) Quel problème réel résolvez-vous?

8
Rob Napier

Cela peut être mieux expliqué avec des protocoles comme Equatable. Vous ne pouvez pas déclarer un tableau [Equatable] car si deux instances de Int peuvent être comparées et que deux instances de Double peuvent être comparées, vous ne pouvez pas comparer une Int à une Double bien qu'ils implémentent tous les deux Equatable.

MyProtocol est un protocole, ce qui signifie qu'il fournit une interface générique. Malheureusement, vous avez également utilisé Self dans la définition. Cela signifie que chaque type conforme à MyProtocol l'implémentera différemment.

Vous l'avez écrit vous-même - Int aura value comme var value: Int tandis qu'un MyObject aura value comme var value: MyObject.

Cela signifie qu'une structure/classe conforme à MyProtocol ne peut pas être utilisée à la place d'une autre structure/classe conforme à MyProtocol. Cela signifie également que vous ne pouvez pas utiliser MyProtocol de cette manière, sans spécifier de type concret.

Si vous remplacez ce Self par un type concret, par exemple AnyObject, cela fonctionnera. Cependant, actuellement (Xcode 6.3.1), il déclenche une erreur de segmentation lors de la compilation).

3
Sulthan

Si vous essayez cet exemple modifié dans une aire de jeux, il se bloquera systématiquement:

// Obviously a very pointless protocol...
protocol MyProtocol {
    var value: Int { get }
}

extension Int   : MyProtocol {  var value: Int    { return self } }
//extension Double: MyProtocol {  var value: Double { return self } }

class Container<T: MyProtocol> {
    var values: [T]

    init(_ values: T...) {
        self.values = values
    }
}


var containers: [Container<MyProtocol>] = []

Ils y travaillent probablement encore, et les choses pourraient changer à l'avenir. Quoi qu'il en soit, pour l'instant, mon explication est qu'un protocole n'est pas un type concret. Ainsi, vous ne savez pas maintenant combien d'espace dans un ram quelque chose de conforme au protocole prendra (par exemple un Int pourrait ne pas occuper la même quantité de ram qu'un Double). Ainsi, il pourrait être assez difficile de répartir le tableau dans ram. En utilisant un NSArray vous allouez un tableau de pointeurs (pointeurs vers NSObjects) et ils occupent tous la même quantité de ram. Vous pouvez considérer le NSArray comme un tableau du type concret "pointeur vers NSObject". Aucun problème de calcul d'allocation de RAM.

Considérez que Array ainsi que Dictionary dans Swift sont Generic Struct, pas objets contenant des pointeurs vers des objets comme dans Obj-C.

J'espère que cela t'aides.

1
Matteo Piombo