如何解决 Swift 不支持第一个 class 元类型的问题?
How to work around Swift not supporting first class meta types?
所以我正在实施以下内容:
- 一个简单的
LanguageType
协议,符合Hashable
- 一个
Translateable
协议,它应该允许你从字典中获取(和设置)一个[String]
,使用LanguageType
作为key
// 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 }
}
和往常一样,Swift对LanguageType
协议的使用方式有问题:
根据我的阅读,这与 Swift 不支持 Existentials 有关,这导致协议实际上不是第一种 class 类型。
在泛型的上下文中,这个问题通常可以用类型擦除的包装器来解决。
就我而言,虽然没有泛型或关联类型。
我想实现的是让translations.Key
成为anyLanguageType
,而不只是一个符合LanguageType
的泛型。
因此,例如,这是行不通的:
protocol Translateable {
typealias Language: LanguageType
var translations: [Language: [String]] { get set }
}
出于某种原因,我只是想不出一种方法来实现这一目标。我发现听起来我需要某种类型擦除的包装器,正如我想要的那样
translations.Key
to be any LanguageType
我想我需要删除确切的类型,它应该符合 Translateable
中的 LanguageType
。
我该怎么做才能解决这个问题?
更新 1:
正如刚刚在 中确定的那样,LanguageType
实际上具有关联的类型要求(使其符合 Equatable
)。因此,我将尝试围绕 LanguageType
.
创建一个类型擦除的包装器
更新二:
所以我意识到,为 LanguageType
创建一个类型擦除的包装器实际上并不能解决问题。我创建了 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
}
如果我现在用它代替 LanguageType
它不会做太多,因为 Translateable
仍然需要关联类型:
protocol Translateable {
typealias T
var translations: [AnyLanguage<T>: [String]] { get set }
}
解决方案:
我从 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 }
}
不确定我为什么要在更新 2 中引入 T
,因为它没有任何作用。但这现在似乎有效。
您不能将协议作为 Dictionary
的密钥,请参阅 Swift Dictionary with Protocol Type as Key。 Swift 需要将字典键绑定到具体类型。
您似乎正试图在同一构造(Translateable
协议)中实现静态多态性和动态多态性,我不确定是否可以实现。
解决方法是将 Translateable
声明为通用结构:
struct Translateable<T: LanguageType> {
var translations: [T: [String]]
}
也许您可以使用符合 LanguageType
的 enum
可以模仿您正在寻找的行为。在这种情况下,您无需在 LanguageType
中明确包含对 hashable
的遵从性,因为枚举是 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
}
}
}
}
示例:
/* 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>"] */
另一种解决方法是使用类似 enum
的 class,您可以在其中 "dynamically add members" 到这个虚构的枚举
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
}
}
}
}
用法示例
/* 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"] */
解决方案似乎是 type-erased wrapper. Type-erasure fixes the problem of not being able to use protocols with associated types (PATs) 作为 first-class 公民,通过创建一个包装器类型,它只公开它包装的协议定义的属性。
在这种情况下,LanguageType
是 PAT,因为它采用了 Equatable
(它符合它,因为它采用了 Hashable)
:
protocol LanguageType: Hashable { /*...*/ }
因此它不能用作 Translatable
协议中的 first-class 类型:
protocol Translatable {
var translations: [LanguageType: [String]] { get set } // error
}
为 Translatable
定义关联类型无法解决问题,因为这会将 LanguageType
限制为一种特定类型:
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
//...
}
如前所述,解决方案是 type-erased 包装器 AnyLanguage
(Apple 对其 type-erased 包装器使用相同的命名约定。例如 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 }
}
此实现现在允许执行以下操作:
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())]
不同类型,符合LanguageType
,现在可以作为Key
使用。唯一的语法差异是 AnyLanguage
:
的必要初始化
AnyLanguage(English())
所以我正在实施以下内容:
- 一个简单的
LanguageType
协议,符合Hashable
- 一个
Translateable
协议,它应该允许你从字典中获取(和设置)一个[String]
,使用LanguageType
作为key
// 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 }
}
和往常一样,Swift对LanguageType
协议的使用方式有问题:
根据我的阅读,这与 Swift 不支持 Existentials 有关,这导致协议实际上不是第一种 class 类型。
在泛型的上下文中,这个问题通常可以用类型擦除的包装器来解决。
就我而言,虽然没有泛型或关联类型。
我想实现的是让translations.Key
成为anyLanguageType
,而不只是一个符合LanguageType
的泛型。
因此,例如,这是行不通的:
protocol Translateable {
typealias Language: LanguageType
var translations: [Language: [String]] { get set }
}
出于某种原因,我只是想不出一种方法来实现这一目标。我发现听起来我需要某种类型擦除的包装器,正如我想要的那样
translations.Key
to be anyLanguageType
我想我需要删除确切的类型,它应该符合 Translateable
中的 LanguageType
。
我该怎么做才能解决这个问题?
更新 1:
正如刚刚在 LanguageType
实际上具有关联的类型要求(使其符合 Equatable
)。因此,我将尝试围绕 LanguageType
.
更新二:
所以我意识到,为 LanguageType
创建一个类型擦除的包装器实际上并不能解决问题。我创建了 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
}
如果我现在用它代替 LanguageType
它不会做太多,因为 Translateable
仍然需要关联类型:
protocol Translateable {
typealias T
var translations: [AnyLanguage<T>: [String]] { get set }
}
解决方案:
我从 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 }
}
不确定我为什么要在更新 2 中引入 T
,因为它没有任何作用。但这现在似乎有效。
您不能将协议作为 Dictionary
的密钥,请参阅 Swift Dictionary with Protocol Type as Key。 Swift 需要将字典键绑定到具体类型。
您似乎正试图在同一构造(Translateable
协议)中实现静态多态性和动态多态性,我不确定是否可以实现。
解决方法是将 Translateable
声明为通用结构:
struct Translateable<T: LanguageType> {
var translations: [T: [String]]
}
也许您可以使用符合 LanguageType
的 enum
可以模仿您正在寻找的行为。在这种情况下,您无需在 LanguageType
中明确包含对 hashable
的遵从性,因为枚举是 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
}
}
}
}
示例:
/* 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>"] */
另一种解决方法是使用类似 enum
的 class,您可以在其中 "dynamically add members" 到这个虚构的枚举
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
}
}
}
}
用法示例
/* 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"] */
解决方案似乎是 type-erased wrapper. Type-erasure fixes the problem of not being able to use protocols with associated types (PATs) 作为 first-class 公民,通过创建一个包装器类型,它只公开它包装的协议定义的属性。
在这种情况下,LanguageType
是 PAT,因为它采用了 Equatable
(它符合它,因为它采用了 Hashable)
:
protocol LanguageType: Hashable { /*...*/ }
因此它不能用作 Translatable
协议中的 first-class 类型:
protocol Translatable {
var translations: [LanguageType: [String]] { get set } // error
}
为 Translatable
定义关联类型无法解决问题,因为这会将 LanguageType
限制为一种特定类型:
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
//...
}
如前所述,解决方案是 type-erased 包装器 AnyLanguage
(Apple 对其 type-erased 包装器使用相同的命名约定。例如 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 }
}
此实现现在允许执行以下操作:
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())]
不同类型,符合LanguageType
,现在可以作为Key
使用。唯一的语法差异是 AnyLanguage
:
AnyLanguage(English())