Come utilizzare il protocollo generico come tipo di variabile


89

Diciamo che ho un protocollo:

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

Ed ecco l'implementazione

class Printer<T> : Printable {

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

La mia aspettativa era che dovevo essere in grado di utilizzare la Printablevariabile per stampare valori come questo:

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

Il compilatore si lamenta di questo errore:

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

Sto facendo qualcosa di sbagliato ? Comunque per risolvere questo problema?

**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: esempio del mondo reale di ciò che voglio. Nota che questo non verrà compilato, ma presenta ciò che voglio ottenere.

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

1
Di seguito è riportato un thread pertinente sui forum degli sviluppatori Apple del 2014 in cui questa domanda viene affrontata (in una certa misura) da uno sviluppatore Swift di Apple: devforums.apple.com/thread/230611 (Nota: per visualizzarlo è necessario un account sviluppatore Apple pagina.)
titaniumdecoy

Risposte:


88

Come sottolinea Thomas, puoi dichiarare la tua variabile non dando affatto un tipo (o potresti specificarla esplicitamente come tipo Printer<Int>. Ma ecco una spiegazione del motivo per cui non puoi avere un tipo di Printableprotocollo.

Non è possibile trattare i protocolli con tipi associati come i normali protocolli e dichiararli come tipi di variabili autonomi. Per pensare al perché, considera questo scenario. Supponiamo di aver dichiarato un protocollo per memorizzare un tipo arbitrario e quindi recuperarlo:

// 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, finora tutto bene.

Ora, il motivo principale per cui un tipo di variabile dovrebbe essere un protocollo implementato da un tipo, piuttosto che il tipo effettivo, è che puoi assegnare diversi tipi di oggetto che sono tutti conformi a quel protocollo alla stessa variabile e ottenere polimorfismo comportamento in fase di esecuzione a seconda di ciò che l'oggetto è effettivamente.

Ma non puoi farlo se il protocollo ha un tipo associato. Come funzionerebbe in pratica il codice seguente?

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

Nel codice sopra, quale sarebbe il tipo di x? An Int? Oppure un String? In Swift, tutti i tipi devono essere corretti in fase di compilazione. Una funzione non può passare dinamicamente dalla restituzione di un tipo a un altro in base a fattori determinati in fase di esecuzione.

Invece, puoi usare solo StoredTypecome vincolo generico. Supponi di voler stampare qualsiasi tipo di tipo memorizzato. Potresti scrivere una funzione come questa:

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

printStoredValue(intStorer)
printStoredValue(stringStorer)

Questo va bene, perché in fase di compilazione, è come se il compilatore scrivesse due versioni di printStoredValue: una per Ints e una per Strings. All'interno di queste due versioni, xè noto per essere di un tipo specifico.


20
In altre parole, non c'è modo di avere un protocollo generico come parametro e il motivo è che Swift non supporta il supporto di runtime in stile .NET dei generici? Questo è abbastanza scomodo.
Tamerlano il

La mia conoscenza di .NET è un po 'confusa ... hai un esempio di qualcosa di simile in .NET che potrebbe funzionare in questo esempio? Inoltre, è un po 'difficile vedere cosa ti sta comprando il protocollo nel tuo esempio. In fase di esecuzione, quale sarebbe il comportamento se si assegnassero stampanti di diversi tipi alla pvariabile e poi si passassero i tipi non validi print? Eccezione di runtime?
Velocità relativa

@AirspeedVelocity Ho aggiornato la domanda per includere un esempio C #. Bene, per quanto riguarda il valore per cui ho bisogno è che questo mi permetterà di sviluppare un'interfaccia non all'implementazione. Se ho bisogno di passare stampabile a una funzione, posso usare l'interfaccia nella dichiarazione e passare molte implementazioni di differenza senza toccare la mia funzione. Pensa anche all'implementazione della libreria di raccolta, avrai bisogno di questo tipo di codice più vincoli aggiuntivi sul tipo T.
Tamerlano

4
Teoricamente, se fosse possibile creare protocolli generici utilizzando parentesi angolari come in C #, sarebbe consentita la creazione di variabili di tipo protocollo? (StoringType <Int>, StoringType <String>)
GeRyCh

1
In Java potresti fare l'equivalente di var someStorer: StoringType<Int>o var someStorer: StoringType<String>e risolvere il problema che hai delineato.
JeremyP

43

C'è un'altra soluzione che non è stata menzionata su questa domanda, che utilizza una tecnica chiamata cancellazione del tipo . Per ottenere un'interfaccia astratta per un protocollo generico, creare una classe o una struttura che racchiuda un oggetto o una struttura conforme al protocollo. La classe wrapper, solitamente denominata "Any {nome protocollo}", è conforme al protocollo e implementa le sue funzioni inoltrando tutte le chiamate all'oggetto interno. Prova l'esempio seguente in un parco giochi:

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

Il tipo di printerè noto per essere AnyPrinter<Int>e può essere utilizzato per astrarre qualsiasi possibile implementazione del protocollo Printer. Sebbene AnyPrinter non sia tecnicamente astratto, la sua implementazione è solo un passaggio a un vero tipo di implementazione e può essere utilizzata per separare i tipi di implementazione dai tipi che li utilizzano.

Una cosa da notare è che AnyPrinternon è necessario mantenere esplicitamente l'istanza di base. In effetti, non possiamo poiché non possiamo dichiarare AnyPrinterdi avere una Printer<T>proprietà. Invece, otteniamo un puntatore _printalla funzione di base print. Chiamando base.printsenza invocare restituisce una funzione in cui base è curata come la variabile self, e viene quindi conservata per future invocazioni.

Un'altra cosa da tenere a mente è che questa soluzione è essenzialmente un altro livello di invio dinamico che significa un leggero impatto sulle prestazioni. Inoltre, l'istanza di cancellazione del tipo richiede memoria aggiuntiva sopra l'istanza sottostante. Per questi motivi, la cancellazione del tipo non è un'astrazione gratuita.

Ovviamente c'è del lavoro per impostare la cancellazione del tipo, ma può essere molto utile se è necessaria l'astrazione del protocollo generico. Questo modello si trova nella libreria standard rapida con tipi come AnySequence. Ulteriori letture: http://robnapier.net/erasure

BONUS:

Se decidi di voler iniettare la stessa implementazione di Printerovunque, puoi fornire un comodo inizializzatore per il AnyPrinterquale inietta quel tipo.

extension AnyPrinter {

    convenience init() {

        let nsLogger = NSLogger<T>()

        self.init(base: nsLogger)
    }
}

let printer = AnyPrinter<Int>()

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

Questo può essere un modo semplice e DRY per esprimere le iniezioni di dipendenza per i protocolli che usi nella tua app.


Grazie per questo. Mi piace questo modello di cancellazione del tipo (usando i puntatori a funzione) meglio dell'uso di una classe astratta (che ovviamente non esiste e deve essere falsificata usando fatalError()) che è descritta in altri tutorial sulla cancellazione di tipo.
Inseguimento

4

Affrontare il tuo caso d'uso aggiornato:

(btw Printableè già un protocollo Swift standard, quindi probabilmente vorresti scegliere un nome diverso per evitare confusione)

Per applicare restrizioni specifiche agli implementatori del protocollo, è possibile limitare i tipi di protocollo. Quindi, per creare la tua raccolta di protocolli che richiede che gli elementi siano stampabili:

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

Ora, se volessi implementare una raccolta che potesse contenere solo elementi stampabili:

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

Tuttavia, questo è probabilmente di scarsa utilità effettiva, dal momento che non puoi vincolare le strutture di raccolta Swift esistenti in questo modo, solo quelle che implementi.

Invece, dovresti creare funzioni generiche che vincolano il loro input a raccolte contenenti elementi stampabili.

func printCollection
    <C: CollectionType where C.Generator.Element: Printable>
    (source: C) {
        for x in source {
            x.print()
        }
}

Oh amico, questo sembra malato. Quello di cui avevo bisogno era solo avere un protocollo con supporto generico. Speravo di avere qualcosa del genere: protocol Collection <T>: SequenceType. E questo è tutto. Grazie per gli esempi di codice, penso che ci vorrà un po 'per digerirlo :)
Tamerlane
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.