Verwendung des generischen Protokolls als Variablentyp

87

Angenommen, ich habe ein Protokoll:

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

Und hier ist die Implementierung

class Printer<T> : Printable {

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

Meine Erwartung war, dass ich in der Lage sein muss, PrintableVariablen zum Drucken von Werten wie diesen zu verwenden:

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

Der Compiler beschwert sich über diesen Fehler:

"Das Protokoll 'Druckbar' kann nur als allgemeine Einschränkung verwendet werden, da es Selbst- oder zugehörige Typanforderungen hat."

Mache ich etwas falsch ? Wie auch immer, um das zu beheben?

**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: Beispiel aus der Praxis für das, was ich will. Beachten Sie, dass dies nicht kompiliert wird, sondern das darstellt, was ich erreichen möchte.

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()
}
Tamerlane
quelle
1
Hier ist ein relevanter Thread in den Apple Developer Forums aus dem Jahr 2014, in dem diese Frage (bis zu einem gewissen Grad) von einem Swift-Entwickler bei Apple beantwortet wird : devforums.apple.com/thread/230611 (Hinweis: Zum Anzeigen ist ein Apple Developer Account erforderlich Seite.)
Titandecoy

Antworten:

85

Wie Thomas betont, können Sie Ihre Variable deklarieren, indem Sie überhaupt keinen Typ angeben (oder Sie können ihn explizit als Typ Printer<Int>angeben. Hier finden Sie eine Erklärung, warum Sie keinen Typ des PrintableProtokolls haben können.

Sie können Protokolle mit zugeordneten Typen nicht wie reguläre Protokolle behandeln und sie als eigenständige Variablentypen deklarieren. Betrachten Sie dieses Szenario, um darüber nachzudenken, warum. Angenommen, Sie haben ein Protokoll zum Speichern und Abrufen eines beliebigen Typs deklariert:

// 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, soweit so gut.

Der Hauptgrund dafür, dass ein Variablentyp ein Protokoll ist, das ein Typ implementiert, und nicht der tatsächliche Typ, besteht darin, dass Sie derselben Variablen verschiedene Objekttypen zuweisen können, die alle diesem Protokoll entsprechen, und polymorph werden Verhalten zur Laufzeit abhängig davon, was das Objekt tatsächlich ist.

Sie können dies jedoch nicht tun, wenn dem Protokoll ein Typ zugeordnet ist. Wie würde der folgende Code in der Praxis funktionieren?

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

Was wäre im obigen Code die Art von x? Ein Int? Oder ein String? In Swift müssen alle Typen zur Kompilierungszeit festgelegt werden. Eine Funktion kann nicht dynamisch von der Rückgabe eines Typs zu einem anderen wechseln, basierend auf zur Laufzeit bestimmten Faktoren.

Stattdessen können Sie nur StoredTypeals allgemeine Einschränkung verwenden. Angenommen, Sie möchten einen gespeicherten Typ ausdrucken. Sie könnten eine Funktion wie diese schreiben:

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

printStoredValue(intStorer)
printStoredValue(stringStorer)

Dies ist in Ordnung, da es beim Kompilieren so ist, als würde der Compiler zwei Versionen von ausschreiben printStoredValue: eine für Ints und eine für Strings. Innerhalb dieser beiden Versionen xist bekannt, dass es sich um einen bestimmten Typ handelt.

Fluggeschwindigkeit
quelle
20
Mit anderen Worten, es gibt keine Möglichkeit, ein generisches Protokoll als Parameter zu verwenden, und der Grund dafür ist, dass Swift die Laufzeitunterstützung von Generika im .NET-Stil nicht unterstützt. Das ist ziemlich unpraktisch.
Tamerlane
Mein .NET-Wissen ist etwas verschwommen ... haben Sie ein Beispiel für etwas Ähnliches in .NET, das in diesem Beispiel funktionieren würde? Außerdem ist es etwas schwierig zu erkennen, was das Protokoll in Ihrem Beispiel für Sie bedeutet. Was würden Sie zur Laufzeit erwarten, wenn Sie Ihrer pVariablen Drucker unterschiedlichen Typs zuweisen und dann ungültige Typen übergeben würden print? Laufzeitausnahme?
Fluggeschwindigkeit
@AirspeedVelocity Ich habe die Frage so aktualisiert, dass sie ein C # -Beispiel enthält. Der Wert, den ich brauche, besteht darin, dass ich mich zu einer Schnittstelle entwickeln kann, die nicht zur Implementierung gehört. Wenn ich printable an eine Funktion übergeben muss, kann ich die Schnittstelle in der Deklaration verwenden und viele unterschiedliche Implementierungen übergeben, ohne meine Funktion zu berühren. Denken Sie auch an die Implementierung der Sammlungsbibliothek. Sie benötigen diese Art von Code sowie zusätzliche Einschränkungen für Typ T.
Tamerlane
4
Wenn es theoretisch möglich wäre, generische Protokolle mit spitzen Klammern wie in C # zu erstellen, wäre die Erstellung von Variablen des Protokolltyps theoretisch zulässig? (StoringType <Int>, StoringType <String>)
GeRyCh
1
In Java können Sie das Äquivalent zu var someStorer: StoringType<Int>oder var someStorer: StoringType<String>ausführen und das von Ihnen skizzierte Problem lösen.
JeremyP
41

Es gibt eine weitere Lösung, die in dieser Frage nicht erwähnt wurde, nämlich die Verwendung einer Technik namens Typlöschung . Um eine abstrakte Schnittstelle für ein generisches Protokoll zu erhalten, erstellen Sie eine Klasse oder Struktur, die ein Objekt oder eine Struktur umschließt, die dem Protokoll entspricht. Die Wrapper-Klasse, normalerweise 'Any {Protokollname}' genannt, entspricht selbst dem Protokoll und implementiert seine Funktionen, indem alle Aufrufe an das interne Objekt weitergeleitet werden. Probieren Sie das folgende Beispiel auf einem Spielplatz aus:

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

Der Typ von printerist bekannt AnyPrinter<Int>und kann verwendet werden, um jede mögliche Implementierung des Druckerprotokolls zu abstrahieren. Obwohl AnyPrinter technisch nicht abstrakt ist, ist seine Implementierung nur ein Durchfall zu einem echten Implementierungstyp und kann verwendet werden, um Implementierungstypen von den Typen zu entkoppeln, die sie verwenden.

Zu beachten ist, dass die AnyPrinterBasisinstanz nicht explizit beibehalten werden muss. In der Tat können wir nicht, da wir nicht erklären können AnyPrinter, eine Printer<T>Immobilie zu haben . Stattdessen erhalten wir einen Funktionszeiger _printauf die printFunktion der Basis . Aufruf base.printohne es den Aufruf gibt eine Funktion , wo Base als Selbst Variable curried wird und auf diese Weise für zukünftige Anrufungen beibehalten.

Eine andere Sache, die Sie beachten sollten, ist, dass diese Lösung im Wesentlichen eine weitere Schicht des dynamischen Versands darstellt, was einen leichten Leistungseinbruch bedeutet. Außerdem benötigt die Instanz zum Löschen von Typen zusätzlichen Speicher über der zugrunde liegenden Instanz. Aus diesen Gründen ist das Löschen von Typen keine kostenlose Abstraktion.

Natürlich gibt es einige Arbeiten zum Einrichten der Typlöschung, aber es kann sehr nützlich sein, wenn eine generische Protokollabstraktion erforderlich ist. Dieses Muster befindet sich in der schnellen Standardbibliothek mit Typen wie AnySequence. Weiterführende Literatur: http://robnapier.net/erasure

BONUS:

Wenn Sie entscheiden, dass Sie Printerüberall dieselbe Implementierung injizieren möchten , können Sie einen praktischen Initialisierer bereitstellen, für AnyPrinterden dieser Typ injiziert wird.

extension AnyPrinter {

    convenience init() {

        let nsLogger = NSLogger<T>()

        self.init(base: nsLogger)
    }
}

let printer = AnyPrinter<Int>()

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

Dies kann eine einfache und trockene Methode sein, um Abhängigkeitsinjektionen für Protokolle auszudrücken, die Sie in Ihrer App verwenden.

Patrick Goley
quelle
Danke dafür. Ich mag dieses Muster der Typlöschung (unter Verwendung von Funktionszeigern) besser als die Verwendung einer abstrakten Klasse (die natürlich nicht existiert und mit der gefälscht werden muss fatalError()), die in anderen Tutorials zur Typlöschung beschrieben wird.
Chase
4

Adressierung Ihres aktualisierten Anwendungsfalls:

(Übrigens Printableist es bereits ein Standard-Swift-Protokoll, daher möchten Sie wahrscheinlich einen anderen Namen auswählen, um Verwirrung zu vermeiden.)

Um bestimmte Einschränkungen für Protokollimplementierer durchzusetzen, können Sie die Typealien des Protokolls einschränken. So erstellen Sie Ihre Protokollsammlung, für die die Elemente druckbar sein müssen:

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

Wenn Sie nun eine Sammlung implementieren möchten, die nur druckbare Elemente enthalten kann:

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

Dies ist jedoch wahrscheinlich von geringem Nutzen, da Sie vorhandene Swift-Auflistungsstrukturen nicht so einschränken können, sondern nur solche, die Sie implementieren.

Stattdessen sollten Sie generische Funktionen erstellen, die ihre Eingabe auf Sammlungen beschränken, die druckbare Elemente enthalten.

func printCollection
    <C: CollectionType where C.Generator.Element: Printable>
    (source: C) {
        for x in source {
            x.print()
        }
}
Fluggeschwindigkeit
quelle
Oh Mann, das sieht krank aus. Was ich brauchte, war nur ein Protokoll mit allgemeiner Unterstützung. Ich hatte gehofft, so etwas zu haben: protocol Collection <T>: SequenceType. Und das ist es. Vielen Dank für die Codebeispiele, ich denke, es wird eine Weile dauern, um es zu verdauen :)
Tamerlane