Comment utiliser le protocole générique comme type de variable

89

Disons que j'ai un protocole:

public protocol Printable {
    typealias T
    func Print(val:T)
}

Et voici la mise en œuvre

class Printer<T> : Printable {

    func Print(val: T) {
        println(val)
    }
}

Je m'attendais à ce que je puisse utiliser une Printablevariable pour imprimer des valeurs comme celle-ci:

let p:Printable = Printer<Int>()
p.Print(67)

Le compilateur se plaint de cette erreur:

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

Est-ce que je fais quelque chose de mal ? Aucun moyen de réparer cela ?

**EDIT :** Adding similar code that works in C#

public interface IPrintable<T> 
{
    void Print(T val);
}

public class Printer<T> : IPrintable<T>
{
   public void Print(T val)
   {
      Console.WriteLine(val);
   }
}


//.... inside Main
.....
IPrintable<int> p = new Printer<int>();
p.Print(67)

EDIT 2: exemple du monde réel de ce que je veux. Notez que cela ne compilera pas, mais présente ce que je veux réaliser.

protocol Printable 
{
   func Print()
}

protocol CollectionType<T where T:Printable> : SequenceType 
{
   .....
   /// here goes implementation
   ..... 
}

public class Collection<T where T:Printable> : CollectionType<T>
{
    ......
}

let col:CollectionType<Int> = SomeFunctiionThatReturnsIntCollection()
for item in col {
   item.Print()
}
Tamerlan
la source
1
Voici un fil de discussion pertinent sur les forums des développeurs Apple de 2014 où cette question est abordée (dans une certaine mesure) par un développeur Swift chez Apple: devforums.apple.com/thread/230611 (Remarque: un compte de développeur Apple est requis pour afficher ceci page.)
titaniumdecoy

Réponses:

88

Comme Thomas le souligne, vous pouvez déclarer votre variable en ne donnant pas du tout un type (ou vous pouvez le donner explicitement comme type Printer<Int>. Mais voici une explication de la raison pour laquelle vous ne pouvez pas avoir de type de Printableprotocole.

Vous ne pouvez pas traiter les protocoles avec des types associés comme des protocoles réguliers et les déclarer comme des types de variables autonomes. Pour réfléchir à pourquoi, considérez ce scénario. Supposons que vous ayez déclaré un protocole pour stocker un type arbitraire, puis le récupérer:

// a general protocol that allows for storing and retrieving
// a specific type (as defined by a Stored typealias
protocol StoringType {
    typealias Stored

    init(_ value: Stored)
    func getStored() -> Stored
}

// An implementation that stores Ints
struct IntStorer: StoringType {
    typealias Stored = Int
    private let _stored: Int
    init(_ value: Int) { _stored = value }
    func getStored() -> Int { return _stored }
}

// An implementation that stores Strings
struct StringStorer: StoringType {
    typealias Stored = String
    private let _stored: String
    init(_ value: String) { _stored = value }
    func getStored() -> String { return _stored }
}

let intStorer = IntStorer(5)
intStorer.getStored() // returns 5

let stringStorer = StringStorer("five")
stringStorer.getStored() // returns "five"

OK, jusqu'ici tout va bien.

Maintenant, la principale raison pour laquelle vous auriez un type de variable soit un protocole qu'un type implémente, plutôt que le type réel, est que vous pouvez assigner différents types d'objets qui sont tous conformes à ce protocole à la même variable, et devenir polymorphe comportement à l'exécution en fonction de ce qu'est réellement l'objet.

Mais vous ne pouvez pas faire cela si le protocole a un type associé. Comment le code suivant fonctionnerait-il en pratique?

// as you've seen this won't compile because
// StoringType has an associated type.

// randomly assign either a string or int storer to someStorer:
var someStorer: StoringType = 
      arc4random()%2 == 0 ? intStorer : stringStorer

let x = someStorer.getStored()

Dans le code ci-dessus, quel serait le type de x? Un Int? Ou un String? Dans Swift, tous les types doivent être corrigés au moment de la compilation. Une fonction ne peut pas passer dynamiquement du retour d'un type à un autre en fonction de facteurs déterminés lors de l'exécution.

Au lieu de cela, vous ne pouvez utiliser StoredTypeque comme contrainte générique. Supposons que vous vouliez imprimer n'importe quel type de type stocké. Vous pouvez écrire une fonction comme celle-ci:

func printStoredValue<S: StoringType>(storer: S) {
    let x = storer.getStored()
    println(x)
}

printStoredValue(intStorer)
printStoredValue(stringStorer)

C'est OK, car au moment de la compilation, c'est comme si le compilateur écrivait deux versions de printStoredValue: une pour Ints et une pour Strings. Dans ces deux versions, xest connu pour être d'un type spécifique.

Vitesse de l'air
la source
20
En d'autres termes, il n'y a aucun moyen d'avoir un protocole générique comme paramètre et la raison en est que Swift ne prend pas en charge la prise en charge des génériques à l'exécution de style .NET? C'est assez gênant.
Tamerlane
Ma connaissance de .NET est un peu floue ... avez-vous un exemple de quelque chose de similaire dans .NET qui fonctionnerait dans cet exemple? En outre, il est un peu difficile de voir ce que le protocole de votre exemple vous achète. Au moment de l'exécution, quel comportement attendriez-vous si vous affectiez des imprimantes de types différents à votre pvariable, puis passiez des types non valides dans print? Exception d'exécution?
Airspeed Velocity
@AirspeedVelocity J'ai mis à jour la question pour inclure l'exemple C #. Quant à la valeur pour laquelle j'en ai besoin, c'est que cela me permettra de développer vers une interface et non vers l'implémentation. Si j'ai besoin de passer imprimable à une fonction, je peux utiliser l'interface dans la déclaration et transmettre de nombreuses implémentations de différence sans toucher à ma fonction. Pensez également à implémenter la bibliothèque de collection, vous aurez besoin de ce type de code plus des contraintes supplémentaires sur le type T.
Tamerlane
4
Théoriquement, s'il était possible de créer des protocoles génériques en utilisant des chevrons comme en C #, la création de variables de type protocole serait-elle autorisée? (StoringType <Int>, StoringType <String>)
GeRyCh
1
En Java, vous pouvez faire l'équivalent de var someStorer: StoringType<Int>ou var someStorer: StoringType<String>et résoudre le problème que vous décrivez.
JeremyP
42

Il y a une autre solution qui n'a pas été mentionnée sur cette question, qui utilise une technique appelée effacement de type . Pour obtenir une interface abstraite pour un protocole générique, créez une classe ou une structure qui encapsule un objet ou une structure conforme au protocole. La classe wrapper, généralement nommée «Any {protocol name}», se conforme elle-même au protocole et implémente ses fonctions en transférant tous les appels à l'objet interne. Essayez l'exemple ci-dessous dans une aire de jeux:

import Foundation

public protocol Printer {
    typealias T
    func print(val:T)
}

struct AnyPrinter<U>: Printer {

    typealias T = U

    private let _print: U -> ()

    init<Base: Printer where Base.T == U>(base : Base) {
        _print = base.print
    }

    func print(val: T) {
        _print(val)
    }
}

struct NSLogger<U>: Printer {

    typealias T = U

    func print(val: T) {
        NSLog("\(val)")
    }
}

let nsLogger = NSLogger<Int>()

let printer = AnyPrinter(base: nsLogger)

printer.print(5) // prints 5

Le type de printerest connu AnyPrinter<Int>et peut être utilisé pour résumer toute implémentation possible du protocole d'imprimante. Bien qu'AnyPrinter ne soit pas techniquement abstrait, son implémentation n'est qu'une chute vers un type d'implémentation réel et peut être utilisée pour découpler les types d'implémentation des types qui les utilisent.

Une chose à noter est qu'il AnyPrintern'est pas nécessaire de conserver explicitement l'instance de base. En fait, nous ne pouvons pas car nous ne pouvons pas déclarer AnyPrinteravoir une Printer<T>propriété. Au lieu de cela, nous obtenons un pointeur de fonction _printvers la printfonction de base . L'appel base.printsans l'invoquer renvoie une fonction où la base est curry comme variable auto, et est donc conservée pour les futures invocations.

Une autre chose à garder à l'esprit est que cette solution est essentiellement une autre couche de répartition dynamique, ce qui signifie un léger impact sur les performances. En outre, l'instance d'effacement de type nécessite une mémoire supplémentaire en plus de l'instance sous-jacente. Pour ces raisons, l'effacement de caractères n'est pas une abstraction gratuite.

De toute évidence, il y a du travail pour configurer l'effacement de type, mais cela peut être très utile si une abstraction de protocole générique est nécessaire. Ce modèle se trouve dans la bibliothèque standard swift avec des types comme AnySequence. Lectures complémentaires: http://robnapier.net/erasure

PRIME:

Si vous décidez que vous souhaitez injecter la même implémentation de Printerpartout, vous pouvez fournir un initialiseur pratique pour AnyPrinterlequel injecte ce type.

extension AnyPrinter {

    convenience init() {

        let nsLogger = NSLogger<T>()

        self.init(base: nsLogger)
    }
}

let printer = AnyPrinter<Int>()

printer.print(10) //prints 10 with NSLog

Cela peut être un moyen simple et SEC d'exprimer des injections de dépendances pour les protocoles que vous utilisez dans votre application.

Patrick Goley
la source
Merci pour cela. J'aime mieux ce modèle d'effacement de type (en utilisant des pointeurs de fonction) que d'utiliser une classe abstraite (qui, bien sûr, n'existe pas et doit être simulée avec fatalError()) qui est décrite dans d'autres tutoriels d'effacement de type.
Chase le
4

Répondre à votre cas d'utilisation mis à jour:

(btw Printableest déjà un protocole Swift standard, vous voudrez probablement choisir un nom différent pour éviter toute confusion)

Pour appliquer des restrictions spécifiques aux implémenteurs de protocole, vous pouvez contraindre les typealias du protocole. Donc, pour créer votre collection de protocoles qui nécessite que les éléments soient imprimables:

// because of how how collections are structured in the Swift std lib,
// you’d first need to create a PrintableGeneratorType, which would be
// a constrained version of GeneratorType
protocol PrintableGeneratorType: GeneratorType {
    // require elements to be printable:
    typealias Element: Printable
}

// then have the collection require a printable generator
protocol PrintableCollectionType: CollectionType {
    typealias Generator: PrintableGenerator
}

Maintenant, si vous vouliez implémenter une collection qui ne pouvait contenir que des éléments imprimables:

struct MyPrintableCollection<T: Printable>: PrintableCollectionType {
    typealias Generator = IndexingGenerator<T>
    // etc...
}

Cependant, cela n'a probablement que peu d'utilité réelle, car vous ne pouvez pas contraindre les structures de collection Swift existantes comme celle-ci, uniquement celles que vous implémentez.

Au lieu de cela, vous devez créer des fonctions génériques qui contraignent leur entrée à des collections contenant des éléments imprimables.

func printCollection
    <C: CollectionType where C.Generator.Element: Printable>
    (source: C) {
        for x in source {
            x.print()
        }
}
Vitesse de l'air
la source
Oh mec, ça a l'air malade. Ce dont j'avais besoin, c'est juste d'avoir un protocole avec un support générique. J'espérais avoir quelque chose comme ceci: protocol Collection <T>: SequenceType. Et c'est tout. Merci pour les exemples de code, je pense qu'il faudra un certain temps pour le digérer :)
Tamerlane