Swift Tips 025 - Using associated enum values to avoid state-specific optionals

每天了解一点不一样的 Swift 小知识

代码截图

小笔记

这段代码在说什么

截图里的上半部分是 Player 类型的定义,在这个定义里面,我们看到它使用 5 个属性来表示游戏里的状态和相关数据。

而截图里的下半部分是对 Player 类型 的定义进行了重构,增加了一个嵌套的枚举类型 State ,它将原先的 5 个属性整合在 State 类型里,并使用了一个名为 state 属性来表明游戏状态。

为什么能这么做

首先, Swift 区别于 Objective-C 和其他语言的一个特点就在于它支持嵌套类型。

我们知道枚举常被用于为特定类或结构体实现某些功能。类似地,枚举可以方便的定义工具类或结构体,从而为某个复杂的类型所使用。为了实现这种功能,Swift 支持定义嵌套类型,可以在支持的类型中定义嵌套的枚举、类和结构体。

要在一个类型中嵌套另一个类型,将嵌套类型的定义写在其外部类型的 {} 内,而且可以根据需要定义多级嵌套。

所以我们看到了重构后的 Player 类型里嵌套了一个 State 类型的枚举值。

其次,在 Swift 的枚举值中,我们可以通过关联值和原始值两种方式将一些有用的数据存储在枚举类型中,而且支持的数据类型也十分丰富,不像 Objective-C 一样,只支持整数类型。下面的代码就展示了 Swift 中枚举的关联值和原始值。

// 这是一个使用关联值来描述两种商品条形码的枚举,分别是标有 UPC 格式的一维条形码和标有 QR 码格式的二维码。
enum Barcode {
case upc(Int, Int, Int, Int)
case qrCode(String)
}
// 这是一个使用 ASCII 码作为原始值的枚举。
enum ASCIIControlCharacter: Character {
case tab = "\t"
case lineFeed = "\n"
case carriageReturn = "\r"
}

如何取出枚举里的关联值

在编辑组内审核这篇 tips 的过程中,某位同事突然提出了一个问题:“如果我想把关联值取出来,应该怎么做呢?”

如果你脑海里马上浮现出来了解决办法,那很棒!可是如果你没什么想法的话,不妨接着往下看。

针对下面的枚举类型,最朴素的方式估计是这样取值的:

enum Barcode {
case upc(Int, Int, Int, Int)
case qrCode(String)
}
let productBarcode = Barcode.upc(1, 2, 3, 4)
var result: Any? = nil;
switch productBarcode {
case let .upc(a, b, c, d):
result = (a, b, c, d) as Any
case let .qrCode(string):
print(string)
}
print(result!)
//"(1, 2, 3, 4)\n"

当然,你可能觉得这样不够优雅,在 Swift 里面,Enum 是不同于 C 和 Objective-C 里的枚举,我们可以在其定义里添加一个方法,就如 Stack Overflow 上的一个高分回答一样:

enum Barcode {
case upc(Int, Int, Int, Int)
case qrCode(String)
func associatedValue() -> Any{
switch self {
case let .upc(a, b, c, d):
return (a, b, c, d)
case let .qrCode(string):
return string
}
}
}
let productBarcode = Barcode.upc(1, 2, 3, 4)
let anotherResult = productBarcode.associatedValue()
print(anotherResult)
//"(1, 2, 3, 4)\n"

但还有更优雅的方案么?Swift 丰富的语法糖拯救了我们!那就是 if-let-case !

enum Barcode {
case upc(Int, Int, Int, Int)
case qrCode(String)
}
let productBarcode = Barcode.upc(1, 2, 3, 4)
if case let Barcode.upc(a, b, c, d) = productBarcode {
print(a, b, c, d)
}
//"1 2 3 4\n"

当然你也可以这么取枚举值

if case let Barcode.upc(a) = productBarcode {
print(a)
}
//"(1, 2, 3, 4)\n"

如果你对 if-let-case 感兴趣,不妨看看这个网站 How Do I Write If Case Let in Swift?

附带的推荐一下这个网站:How Do I Declare a Closure in Swift?

这样做的好处

在原先的 5 个独立属性中,我们很难发现他们之间的关联,但仔细阅读后,我们还是可以发现,这几个属性是互斥的。

就好比 isWaitingForMatchMaking 为 false 的时候,就像喷射战士 2 这种游戏里的对战房间还没创建好,所以像 invitingUser 是不可能有值的,更别说其他的几个属性。

playerDefeatedByroundDefeatedIn 这两个属性其实是存在关联的,我们不可能同时把 playerDefeatedBy 设置为第 3 轮里把我们消灭掉的敌人,而在 roundDefeatedIn 里设置成 1。

所以结合嵌套类型和关联值这两个特性后,我们看到重构后的代码将原有的 5 个属性划分成了游戏的四个状态并相互独立,对于代码逻辑的梳理也变得更加清晰明了。

所以将互斥的业务属性整合到枚举中,这个 tips 你 get 到了么?