2016-01-09 2 views
4

Je suis mise en œuvre de ce qui suit:Comment contourner Swift ne supportant pas les méta-types de première classe?

  • Un protocole LanguageType simple, qui est conforme à Hashable
  • Un protocole Translateable, qui devrait vous permettre d'obtenir (et mettre) un [String] à partir d'un dictionnaire, en utilisant un LanguageType comme la clé

// MARK: - LanguageType 

protocol LanguageType: Hashable { 
    var description: String { get } 
} 

extension LanguageType { 
    var description: String { return "\(Self.self)" } 
    var hashValue: Int { return "\(Self.self)".hashValue } 
} 

func ==<T: LanguageType, U: LanguageType>(left: T, right: U) -> Bool { 
    return left.description == right.description 
} 

// MARK: - Translateable 

protocol Translateable { 
    var translations: [LanguageType: [String]] { get set } 
} 

Comme d'habitude, Swift a un problème avec la façon dont le protocole est utilisé LanguageType:

Error

D'après ce que j'ai lu, cela a à voir avec Swift ne supportant pas Existentials, qui se traduit par des protocoles non étant en fait des types de première classe.

Dans le contexte des génériques, ce problème peut généralement être résolu avec une enveloppe effacée.
Dans mon cas, il n'y a pas de génériques ni de types associés.

Ce que je veux atteindre est d'avoir translations.Key être toutLanguageType, pas seulement un type générique conforme à LanguageType.
Ainsi, par exemple cela ne fonctionnerait pas:

protocol Translateable { 
    typealias Language: LanguageType 

    var translations: [Language: [String]] { get set } 
} 

Pour une raison quelconque, je ne peux pas penser à un moyen d'y parvenir. Je trouve ça sonne comme je l'ai besoin d'une sorte d'emballage effacé type, comme je veux

translations.Key pour être touteLanguageType

Je pense que je dois effacer le type exact, qui est censé pour se conformer à LanguageType en Translateable. Que puis-je faire pour résoudre ce problème?


Mise à jour 1: Comme on vient de déterminer dans this question, LanguageType a fait des exigences de type associé (faire à sa conformité aux Equatable). Par conséquent, je vais essayer de créer un wrapper de type effacé autour de LanguageType.

Mise à jour 2: Je me suis rendu compte que la création d'une enveloppe effacée type pour LanguageType résoudra pas vraiment le problème.J'ai créé AnyLanguage:

struct AnyLanguage<T>: LanguageType { 
    private let _description: String 
    var description: String { return _description } 
    init<U: LanguageType>(_ language: U) { _description = language.description } 
} 

func ==<T, U>(left: AnyLanguage<T>, right: AnyLanguage<U>) -> Bool { 
    return left.description == right.description 
} 

Si je maintenant utilisé à la place de LanguageType il ne ferait pas grand-chose, comme Translateable aurait encore besoin d'un type associé:

protocol Translateable { 
    typealias T 
    var translations: [AnyLanguage<T>: [String]] { get set } 
} 

Solution:

J'ai retiré le générique de AnyLanguage:

struct AnyLanguage: LanguageType { 
    private(set) var description: String 
    init<T: LanguageType>(_ language: T) { description = language.description } 
} 

func ==(left: AnyLanguage, right: AnyLanguage) -> Bool { 
    return left.description == right.description 
} 

protocol Translateable { 
    var translations: [AnyLanguage: [String]] { get set } 
} 

Vous ne savez pas pourquoi j'ai introduit T dans la mise à jour 2, car il ne fait rien. Mais cela semble fonctionner maintenant.

+0

Voulez-vous vraiment à mettre en œuvre l'égalité en comparant les valeurs de hachage? Cela ne marchera jamais. Le problème principal est que vous avez besoin de types concrets. Un dictionnaire ne peut pas fonctionner avec plusieurs classes qui ont différentes implémentations 'hash'. – Sulthan

+0

Un type conforme à 'LanguageType' n'est jamais censé implémenter sa propre version de' hashValue'. Donc 'hashValue' est seulement déclaré comme une extension. L'égalité serait donc basée uniquement sur les noms des langues, ce qui me semble assez bon. –

+0

L'égalité ne peut pas être basée sur des fonctions de hachage. Vous devez toujours comparer les chaînes, pas seulement leurs hachages. – Sulthan

Répondre

0

La solution semble être une type-erased wrapper. L'effacement de type résout le problème de ne pas pouvoir utiliser protocols with associated types (PATs) en tant que citoyens de première classe, en créant un type wrapper, qui expose uniquement les propriétés définies par le protocole, qu'il encapsule.

Dans ce cas, LanguageType est un PAT, en raison de son adoption de Equatable (ce qui est conforme à, en raison de l'adoption de Hashable):

protocol LanguageType: Hashable { /*...*/ } 

Par conséquent, il ne peut pas être utilisé comme une première Type -class dans le protocole Translatable:

protocol Translatable { 
    var translations: [LanguageType: [String]] { get set } // error 
} 

Définition d'un type associé à Translatable ne résoudrait pas le problème, car cela woul d contraindre le LanguageType à être un type spécifique:

protocol Translatable { 
    typealias Language: LanguageType 

    var translations: [Language: [String]] { get set } // works 
}  

struct MyTranslatable<T: LanguageType>: Translatable { 
    var translations: [T: [String]] // `T` can only be one specific type 

    //... 
} 

Comme mentionné la solution est une enveloppe effacée type AnyLanguage (Apple utilise la même convention de dénomination pour les emballages de type effacés. Par exemple AnySequence):

// `AnyLanguage` exposes all of the properties defined by `LanguageType` 
// in this case, there's only the `description` property 
struct AnyLanguage: LanguageType { 
    private(set) var description: String 

    // `AnyLanguage` can be initialized with any type conforming to `LanguageType` 
    init<T: LanguageType>(_ language: T) { description = language.description } 
} 

// needed for `AnyLanguage` to conform to `LanguageType`, as the protocol inherits for `Hashable`, which inherits from `Equatable` 
func ==(left: AnyLanguage, right: AnyLanguage) -> Bool { 
    return left.description == right.description 
} 

// the use of `AnyLanguage` allows any `LanguageType` to be used as the dictionary's `Key`, as long as it is wrapped as `AnyLanguage` 
protocol Translateable { 
    var translations: [AnyLanguage: [String]] { get set } 
} 

Cette mise en œuvre permet désormais les suivantes:

struct SomethingTranslatable: Translatable { 
    var translations: [AnyLanguage: [String]] = [:] 
} 

func ==(left: SomethingTranslatable, right: SomethingTranslatable) -> Bool { /*return some `Bool`*/ } 

struct English: LanguageType { } 
struct German: LanguageType { } 

var something = SomethingTranslatable() 
something.translations[AnyLanguage(English())] = ["Hello", "World"] 
let germanWords = something.translations[AnyLanguage(German())] 

types différents, se conformant à LanguageType, peuvent désormais être utilisés comme Key. La seule différence syntaxique, est l'initialisation nécessaire d'une AnyLanguage:

AnyLanguage(English()) 
3

Vous ne pouvez pas avoir de protocole en tant que clé pour un Dictionary, voir Swift Dictionary with Protocol Type as Key. Swift doit lier la clé du dictionnaire à un type concret.

Semble que vous essayez d'atteindre le polymorphisme statique et le polymorphisme dynamique dans la même construction (le protocole Translateable), dont je ne suis pas sûr qu'il peut être atteint.

Une solution serait de déclarer Translateable comme générique struct:

struct Translateable<T: LanguageType> { 
    var translations: [T: [String]] 
} 
+0

La structure ne fonctionnerait toujours pas, car 'T' finirait par être un type concret, et je veux que' translations.Key' soit un type conforme à 'LanguageType'. –

1

Peut-être vous pouvez utiliser un enum conforme à LanguageType peut imiter le comportement que vous recherchez. Dans ce cas, vous n'avez pas besoin d'inclure explicitement la conformité à hashable dans LanguageType, puisque les enums sont Hashable.

protocol LanguageType { 
    var description: String { get } 
    // ... 
} 

extension LanguageType { 
    var description: String { return "\(Self.self)" } 
} 

enum AnyLanguage : Int, LanguageType { 
    case English = 1, German, Swedish 
    // implement non-default description 
    var description : String { 
     return "Language: " + String(self) 
    } 
} 

protocol Translatable { 
    var myDict : [AnyLanguage:[String]] { get set }//= [:] 
} 

class MyFooWordList : Translatable { 
    private var myBackendDict : [AnyLanguage:[String]] = [:] 
    var myDict : [AnyLanguage:[String]] { 
     get { 
      return myBackendDict 
     } 
     set { 
      for (k, v) in newValue { 
       myBackendDict[k] = v 
      } 
     } 
    } 
} 

Exemple:

/* Example */ 
var myFooWordList = MyFooWordList() 
myFooWordList.myDict = [.English: ["Hello", "World"]] 
myFooWordList.myDict = [.German: ["Hallo", "Welt"]] 

print("Words for '" + AnyLanguage.English.description + "': \(myFooWordList.myDict[.English] ?? ["<Empty>"])") 
/* Words for 'Language: English': ["Hello", "World"] */ 

print("Words for '" + AnyLanguage.German.description + "': \(myFooWordList.myDict[.German] ?? ["<Empty>"])") 
/* Words for 'Language: German': ["Hallo", "Welt"] */ 

print("Words for '" + AnyLanguage.Swedish.description + "': \(myFooWordList.myDict[.Swedish] ?? ["<Empty>"])") 
/* Words for 'Language: Swedish': ["<Empty>"] */ 

Une autre solution consiste à utiliser un enum -comme classe où vous pouvez "ajouter dynamiquement des membres" à ce ENUM fictive

class LanguageType { 

    class AnyLanguage: Hashable { 
     let id: Int 
     let description: String 

     private init(id: Int, description: String) { 
      self.id = id 
      self.description = description 
     } 

     var hashValue: Int { return id } 
    } 

    class var ENGLISH: AnyLanguage { 
     class English: AnyLanguage { 
     } 
     return English(id: 1, description: "English") 
    } 

    class var GERMAN: AnyLanguage { 
     class German: AnyLanguage { 
     } 
     return German(id: 2, description: "German") 
    } 

    class func CUSTOM(id: Int, _ description: String) -> AnyLanguage { 
     return AnyLanguage(id: id, description: description) 
    } 
} 

func == (lhs: LanguageType.AnyLanguage, rhs: LanguageType.AnyLanguage) -> Bool { 
    return lhs.id == rhs.id 
} 

protocol Translatable { 
    var myDict : [LanguageType.AnyLanguage:[String]] { get set }//= [:] 
} 

class MyFooWordList : Translatable { 
    private var myBackendDict : [LanguageType.AnyLanguage:[String]] = [:] 
    var myDict : [LanguageType.AnyLanguage:[String]] { 
     get { 
      return myBackendDict 
     } 
     set { 
      for (k, v) in newValue { 
       myBackendDict[k] = v 
      } 
     } 
    } 
} 

Exemple d'utilisation

/* Example */ 
var myFooWordList = MyFooWordList() 
myFooWordList.myDict = [LanguageType.ENGLISH: ["Hello", "World"]] 
myFooWordList.myDict = [LanguageType.GERMAN: ["Hallo", "Welt"]] 
myFooWordList.myDict = [LanguageType.CUSTOM(3, "Swedish"): ["Hej", "Varlden"]] 
myFooWordList.myDict = [LanguageType.CUSTOM(4, "Finnish"): ["Hei", "Maailma"]] 

print("Words for '" + LanguageType.ENGLISH.description + "': \(myFooWordList.myDict[LanguageType.ENGLISH] ?? ["<Empty>"])") 
/* Words for 'English': ["Hello", "World"] */ 

print("Words for '" + LanguageType.GERMAN.description + "': \(myFooWordList.myDict[LanguageType.GERMAN] ?? ["<Empty>"])") 
/* Words for 'Language: German': ["Hallo", "Welt"] */ 

print("Words for '" + LanguageType.CUSTOM(3, "Swedish").description + "': \(myFooWordList.myDict[LanguageType.CUSTOM(3, "Swedish")] ?? ["<Empty>"])") 
/* Words for 'Swedish': ["Hej", "Varlden"] */ 
+0

Désolé, mais 'Translateable' est vraiment censé être un protocole. Et j'ai également envisagé d'utiliser une énumération stockant les langues. Mais cela supprime le but de 'LanguageType', qui devrait permettre l'ajout de nouvelles langues dans d'autres modules. –

+0

@MarcusRossel J'ai mis à jour ma réponse avec une solution légèrement plus protocolaire, mais toujours juste une énumération. Comme Cristik l'écrit, vous aurez du mal à utiliser un protocole comme un type. – dfri

+0

Vous comprenez évidemment ce que j'essaie d'accomplir. Mais 'AnyLanguage' n'est tout simplement pas faisable dans ce cas, car il supprime toute la flexibilité du code. C'est comme si le 'ErrorType' de Swift n'était pas un protocole auquel vous pouviez vous conformer, mais une énumération répertoriant tous les cas d'erreur possibles. Ça ne marcherait pas. Mais merci d'avoir essayé –