2015-04-22 7 views
39

Ich habe mit Arrays von generischen Klassen mit verschiedenen Typen gespielt. Am einfachsten ist es mein Problem mit einigem Beispielcode zu erklären:Arrays von Generics in Swift

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

Nun, wenn ich versuche, eine Reihe von Containern wie so zu erstellen:

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

ich den Fehler:

Protocol 'MyProtocol' can only be used as a generic constraint because it has Self or associated type requirements.

Um das zu beheben, kann ich [AnyObject] verwenden:

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

Aber jetzt ein weiteres ‚Problem‘ entsteht, wenn durch containers aufzählt:

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

Wie Sie oben im Code sehen können, nachdem die Art des container die gleiche Methode der Bestimmung in beiden Fällen genannt wird. Meine Frage ist:

Gibt es einen besseren Weg, um die Container mit dem richtigen Typ als Gießen zu jeder möglichen Art von Container zu bekommen? Oder habe ich noch etwas übersehen?

Antwort

39

Es gibt einen Weg - Art - zu tun, was Sie wollen - irgendwie. Es gibt einen Weg, mit Protokollen, die Typ Einschränkung zu beseitigen und immer noch das gewünschte Ergebnis zu bekommen, aber es ist nicht immer schön. Hier ist, was ich kam mit als Protokoll in Ihrer Situation:

protocol MyProtocol { 
    func getValue() -> Self 
} 

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

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

Beachten Sie, dass die value Eigenschaft, die Sie ursprünglich in der Protokollerklärung gestellt wurde ein Verfahren geändert, die das Objekt zurückgibt.

Das ist nicht sehr interessant.

Aber jetzt, weil Sie die value Eigenschaft im Protokoll losgeworden sind, kann MyProtocol als ein Typ verwendet werden, nicht nur als eine Art Einschränkung. Ihre Container Klasse muss nicht einmal mehr generisch sein. Sie können es wie folgt erklären:

class Container { 
    var values: [MyProtocol] 

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

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

Und weil Container nicht mehr generisch ist, können Sie eine Array von Container s und durchlaufen sie, den Druck, die Ergebnisse der myMethod() Methode erstellen:

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] 

Der Trick besteht darin, ein Protokoll zu erstellen, das nur allgemeine Funktionen enthält und keine anderen Anforderungen an einen übereinstimmenden Typ stellt. Wenn Sie damit durchkommen können, dann können Sie das Protokoll als einen Typ und nicht nur als eine Art Einschränkung verwenden.

Und als Bonus (wenn Sie es so nennen wollen), kann Ihr Array von MyProtocol Werte sogar verschiedene Typen mischen, die MyProtocol entsprechen. Also, wenn Sie String eine MyProtocol Erweiterung wie folgt geben:

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

Sie können tatsächlich eine Container mit gemischten Typen initialisieren:

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

[Warning - Ich prüfe dies in einem der Online-Spielplätze. Ich habe nicht in der Lage gewesen, es in Xcode zu testen, noch ...]

Edit:

Wenn Sie noch Container wollen generisch sein und nur eine Art von Objekt zu halten, können Sie erreichen, dass durch es machen zu sein eigenes Protokoll entsprechen:

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

Jetzt können Sie noch haben eine Reihe von [ContainerProtocol] Objekte und durchlaufen sie myMethod() Aufruf:

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

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

Vielleicht funktioniert das immer noch nicht für Sie, aber jetzt Container ist auf einen einzigen Typ beschränkt, und Sie können immer noch durch ein Array von ContainterProtocol Objekte durchlaufen.

+0

Im OP war es nie nötig, den Container generisch zu machen. – Sulthan

+0

Wenn das OP wollte, dass Container über ein Array von MyProtocol-Objekten verfügt, musste Container generisch sein, solange MyProtocol über eine erforderliche Werteigenschaft verfügte. –

+0

Vielen Dank für Ihre Antwort.Leider habe ich 'Container' benötigt, um nur einen einzigen Typ zu speichern, aber das ist eine interessante Lösung, und ich bin mir sicher, dass ich später etwas davon dafür finden werde. – ABakerSmith

0

Wenn Sie dieses modifizierten Beispiel auf einem Spielplatz versuchen, wird es systematisch Absturz:

// 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>] = [] 

Wahrscheinlich auf das sie arbeiten immer noch, und die Dinge könnten sich in Zukunft ändern. Wie auch immer, meine Erklärung dafür ist, dass ein Protokoll kein Betontyp ist. Also du nicht jetzt, wie viel Platz in Ram etwas dem Protokoll entsprechen wird (zum Beispiel ein Int möglicherweise nicht die gleiche Menge an RAM wie ein Double besetzen). Daher könnte es ein ziemlich kniffliges Problem sein, das Array im RAM zuzuordnen. Mit einem NSArray Sie sind ein Array von Zeigern (Zeiger auf NSObjects) und sie alle belegen die gleiche Menge an RAM. Sie können sich die NSArray als ein Array der Betontyp "Zeiger auf NSObject" vorstellen. Daher kein Problem bei der Berechnung der RAM-Zuweisung.

Bedenken Sie, dass Array sowie Dictionary in Swift sind Generisches Struct, nicht Objekte Zeiger auf Objekte wie in Obj-C enthält.

Hoffe, das hilft.

-3

Ich änderte die Array-Deklaration zu einem Array von AnyObject, so dass Filter, Map und Reduce verwendet werden konnten (und auch in einigen weiteren Objekten hinzugefügt werden, nach denen gesucht werden soll).

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

Dies ermöglicht es Ihnen, für den Typ in dem Array und Filter, bevor Sie eine Schleife durch das Array

let strings = containers.filter({ return ($0 is String) }) 

println(strings) // [Hello, World] 

for ints in containers.filter({ return ($0 is Int) }) { 
    println("Int is \(foo)") // Int is 42 
} 

let ints = containers.filter({ return ($0 is Container<Int>) }) 
// as this array is known to only contain Container<Int> instances, downcast and unwrap directly 
for i in ints as [Container<Int>] { 
    // do stuff 
    println(i.values) // [1, 2, 3] 
} 
3

Dies kann besser erklärt werden mit Protokollen wie Equatable zu überprüfen.Sie können ein Array [Equatable] nicht deklarieren, da zwei Int Instanzen miteinander verglichen werden können und zwei Instanzen von Double miteinander verglichen werden können, können Sie nicht vergleichen, obwohl beide Equatable implementieren.

MyProtocol ist ein Protokoll, dh es bietet eine generische Schnittstelle. Leider haben Sie in der Definition auch Self verwendet. Das bedeutet, dass jeder Typ, der MyProtocol entspricht, dies anders implementiert.

Sie es selbst geschrieben haben - Int wird value als var value: Int haben, während ein MyObjectvalue als var value: MyObject haben.

Das bedeutet, dass eine Struktur/Klasse, die MyProtocol entspricht, nicht anstelle einer anderen Struktur/Klasse verwendet werden kann, die MyProtocol entspricht. Das bedeutet auch, dass Sie auf diese Weise MyProtocol nicht verwenden können, ohne einen konkreten Typ anzugeben.

Wenn Sie das Self durch einen konkreten Typ ersetzen, z. AnyObject, es wird funktionieren. Derzeit (Xcode 6.3.1) löst es jedoch beim Kompilieren einen Segmentierungsfehler aus.

+1

> Sie können nicht ein Int zu einem Double vergleichen - Sie können, aber Swift fehlt automatische Casting. –

5

Dies ist ein gutes Beispiel für "Was haben Sie wollen passieren?" Und demonstriert tatsächlich die Komplexität, die explodiert, wenn Swift wirklich erstklassige Typen hatte.

protocol MyProtocol { 
    var value: Self { get } 
} 

Großartig. MyProtocol.value gibt den Typ zurück, der implementiert wird, wobei zu beachten ist, dass dies zur Kompilierungszeit und nicht zur Laufzeit bestimmt werden muss.

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

So, bestimmt zur Kompilierzeit, welcher Typ ist das? Vergiss den Compiler, mach es einfach auf dem Papier. Ja, nicht sicher, welcher Typ das wäre. Ich meine Beton Typ. Keine Metatypen.

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

Sie wissen, dass Sie den falschen Weg nach unten, wenn AnyObject in Ihre Signaturen schlich hat. Nichts davon wird jemals funktionieren. Nach AnyObject ist nur Sackleinen.

Or is there something else I've overlooked?

Ja. Sie benötigen einen Typ, und Sie haben keinen angegeben. Sie haben eine Regel zum Einschränken eines Typs, aber keinen tatsächlichen Typ angegeben. Geh zurück zu deinem wirklichen Problem und denke tiefer darüber nach. (Die Metatypanalyse ist fast nie Ihr "echtes" Problem, es sei denn, Sie arbeiten an einem CS PhD, in diesem Fall würden Sie dies in Idris tun, nicht Swift.) Welches eigentliche Problem lösen Sie?

+1

Vielen Dank für Ihre Einsicht. "Welches Problem lösen Sie eigentlich?" ließ mich wieder darüber nachdenken, was ich wirklich erreichen wollte. Mit einem anderen Ansatz, um alles in einem Array zu speichern und etwas zu refactorieren, funktioniert alles ohne die Verwendung von 'AnyObject'. – ABakerSmith

+0

Schnelle Generika sind immer noch roh. Und was in anderen Sprachen einfach ist, ist in Swift immer noch nicht möglich oder zu sperrig. Keine Notwendigkeit, OP zu bitten, zu dem wirklichen Problem zurückzukehren. –

+0

Hey Rob, deine Beobachtung hat mir auch sehr geholfen. Ich kam zu einer Position, wo ich "zu generisch" gegangen bin und anstatt Typen zu verwenden, habe ich einfach "Any" als Workaround verwendet. Ich denke immer noch, ob ich in die falsche Richtung gehe oder Swift ist immer noch nicht "reif" genug, um das zu tun. Trotzdem danke! –