带有关联类型的枚举的原始值初始化构造器

作者:Benedikt Terhechte,原文链接,原文日期:2016/04/23
译者:Lanford3_3;校对:saitjr;定稿:CMB

在 Swift 中,枚举(Enums)是一种优雅的结构化信息的方式。有时候你会发现自己需要通过原始值(raw values)来构造枚举,因为这些值可能零星地存储在某些地方,比如 NSUserDefaults

enum Device: String {
  case Phone, Tablet, Watch
}
let aDevice = Device(rawValue: "Phone")
print(aDevice)

// 打印结果为: Optional(main.Device.Phone)

问题

但只要你在枚举中用了关联值(associated values),这种方式就失效了。比如这样的枚举:

enum Example {
   case Factory(workers: Int)
   case House(street: String)
}

由于这个枚举的两个成员(caseFactoryHouse 有着不同的关联类型(workers 是整型而 street 是字符串),所以 Swift 无法构造出 Example 的实例。Example 各成员的调用需要不同类型的参数,所以这种方法无法通用。

然而,就算所有成员的关联类型都相同,这种方法也不管用:

enum Device {
    case Phone(name: String, screenSize: CGSize)
    case Watch(name: String, screenSize: CGSize)
    case Tablet(name: String, screenSize: CGSize)
}

在这个例子中,所有关联类型(associated types)都是一样的——事实上,有很多种方式都能说明问题,但是我发现用 Device 枚举更能简明扼要的阐述清楚——即使每个 Device 成员的关联类型都是一样的,你仍然无法使用原始值什么的来创建它并得到正确的类型。你需要进行匹配以创建正确的实例:

import Foundation

enum Device {
    case Phone(name: String, screenSize: CGSize)
    case Watch(name: String, screenSize: CGSize)
    case Tablet(name: String, screenSize: CGSize)

    static func fromDefaults(rawValue: String, name: String, screenSize: CGSize) -> Device? {
        switch rawValue {
        case "Phone": return Device.Phone(name: name, screenSize: screenSize)
        case "Watch": return Device.Watch(name: name, screenSize: screenSize)
        case "Tablet": return Device.Tablet(name: name, screenSize: screenSize)
        default: return nil
        }
    }
}
let b = Device.fromDefaults("Phone", name: "iPhone SE", screenSize: CGSize(width: 640, height: 1136))
print(b)

// 打印结果为: Optional(main.Device.phone("iPhone SE", (640.0, 1136.0)))

这看起来还好,但这些代码确实有些冗余了。一旦你需要创建的枚举中有着三个以上的枚举成员或者两种以上的关联类型,事情就会很快失控。

enum Vehicle {
  case Car(wheels: Int, capacity: Int, weight: Int, length: Int, height: Int, width: Int, color: Int, name: Int, producer: Int, creation: NSDate, amountOfProducedUnits: Int)
  case Ship(wheels: Int, capacity: Int, weight: Int, length: Int, height: Int, width: Int, color: Int, name: Int, producer: Int, creation: NSDate, amountOfProducedUnits: Int)
  case Yacht(wheels: Int, capacity: Int, weight: Int, length: Int, height: Int, width: Int, color: Int, name: Int, producer: Int, creation: NSDate, amountOfProducedUnits: Int)
  case Truck(wheels: Int, capacity: Int, weight: Int, length: Int, height: Int, width: Int, color: Int, name: Int, producer: Int, creation: NSDate, amountOfProducedUnits: Int)
  case Motorbike(wheels: Int, capacity: Int, weight: Int, length: Int, height: Int, width: Int, color: Int, name: Int, producer: Int, creation: NSDate, amountOfProducedUnits: Int)
  case Helicopter(wheels: Int, capacity: Int, weight: Int, length: Int, height: Int, width: Int, color: Int, name: Int, producer: Int, creation: NSDate, amountOfProducedUnits: Int)
  case Train(wheels: Int, capacity: Int, weight: Int, length: Int, height: Int, width: Int, color: Int, name: Int, producer: Int, creation: NSDate, amountOfProducedUnits: Int)
  // ...
}

我想你明白我想表达的意思。

解决方法

所以……我们应该怎么处理这种情况呢?有趣的是,在关联类型的初始化构造器(initializer)和闭包间有种迷之相似性。看看下面的代码:

enum Example {
    case Test(x: Int)
}
let exampleClosure = Example.Test

代码中 exampleClosure 的类型是什么?答案是:(Int) -> Example。是的,不添加参数调用一个带有关联类型的枚举成员会生成一个闭包,若是使用正确类型的参数来调用这个闭包,就会返回一个枚举成员实例。

这说明了下面的代码是正确的,能够正常工作:

enum Fruit {
    case Apple(amount: Int)
    case Orange(amount: Int)
}

let appleMaker = Fruit.Apple
let firstApple = appleMaker(amount: 10)
let secondApple = appleMaker(amount: 12)
print(firstApple, secondApple)

// 打印结果为: Apple(10) Apple(12)

所以,这将如何帮助我们简化上面糟糕的代码重复问题呢?让我们来看看:

import Foundation

enum Device {
    case Phone(name: String, screenSize: CGSize)
    case Watch(name: String, screenSize: CGSize)
    case Tablet(name: String, screenSize: CGSize)

    private static var initializers: [String: (name: String, screenSize: CGSize) -> Device] = {
        return ["Phone": Device.Phone, "Watch": Device.Watch, "Tablet": Device.Tablet]
    }()

    static func fromDefaults(rawValue: String, name: String, screenSize: CGSize) -> Device? {
        return Device.initializers[rawValue]?(name: name, screenSize: screenSize)
    }
}

let iPhone = Device.fromDefaults("Phone", name: "iPhone SE", screenSize: CGSize(width: 640, height: 1134))
print(iPhone)

// 打印结果为:Optional(main.Device.Phone("iPhone SE", (640.0, 1134.0)))

让我们试着指出这段代码做了什么。我们给 Device 添加了一个新属性 initializers。这是一个类型为 [String: (name: String, screenSize: CGSize) -> Device] 的字典(Dictionary)。也就是从 String 类型的键(key)映射到和我们的 Device 的成员类型相同的闭包。简单地利用之前的小技巧,通过 Phone: Device.Phone 这样的方式返回闭包,这个字典就包含了我们所有枚举成员的初始化构造器。

之后的 fromDefaults 函数,只需要知道我们想要创建的设备的键值,就可以调用合适的闭包。这使得代码实现精简了许多,尤其是对于一些更大的枚举来说(就像我们上面 Vehicle 的例子)。正如你所看到的,现在创建一个 Device 实例变得非常简单:

Device.initializers["Phone"]?(name: "iPhone 5", screenSize: CGSize(width: 640, height: 1134))

就像直接用原始值那样,如果枚举中没有 Phone 这个成员,则会返回一个空值。

当然,这个解决方案并不完美,我们仍然不得不使用 initializers 这个字典,然而,相较于不得不手动匹配所有的枚举成员,这样做已经减少了大量的重复工作。

最后,我想,不用我多说的是,上面的代码忽视了一点——一个合格的最佳实践应该是简洁并能够让人专注于手上的工作的。然而,像是 Device.initializers["phone"] 这样字符串化的方式并不是最好的写法,这些键应该被合理的定义在别的什么地方。

如果你读到这儿了,我想你应该在 Twitter 上粉我(@terhechte

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 http://swift.gg

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容