您的位置:首页 > 移动开发 > Swift

可以匹配就匹配咯: 详解 Swift 的模式匹配

2015-10-27 10:29 766 查看
怒戳查看最终稿@SwiftGG

在众多 Swift 提供给 Objective-C 程序员使用的新特性中,有个特性把自己伪装成一个无聊的老头,但是却在如何优雅滴解决“鞭尸金字塔“的问题上有着巨大的潜力。很显然我所说的这个特性就是
switch
语句, 对于很多 Objective-C 程序员来说,除了用在 Duff’s Device 上比较有趣之外,
switch
语句非常笨拙,与多个
if
语句相比,它几乎没有任何优势。

不过 Swift 中的
switch
语句能做的就多了。在接下来的教程里,我会更加详细滴讲解这些新特性的各种用途。我会忽略那些与Objective-C 和 C 中
switch
语句相比没有任何优势的解决方案。这篇文章基础的部分写于 2014 年 7 月,但是很多我写的模式都会导致编译器崩溃,所以我只好推迟这些内容的编写,直到编译器能提供更好的支持。

这篇博客还有如下语言的版本:

日语 (感谢 M Ono!)

开始咯

switch
语句主要的特性当然是模式匹配咯,模式匹配可以对值进行解构,然后根据相应
case
的正确匹配值来进行匹配。

// 历史上最坏的一个例子:二进制->十进制的转换
let bool1 = 1
let bool2 = 0
switch (bool1, bool2) {
case (0, 0): print("0")
case (0, 1): print("1")
case (1, 0): print("2")
case (1, 1): print("3")
}


模式匹配很早以前就在其他语言中存在了,这些语言包括 Haskell、Erlang、Scala和Prolog。这是一个福音,因为这允许我们观察那些语言是如何利用模式匹配来解决它们的问题的。我们甚至可以通过观察它们的例子来找到最实用的那个。

一个交易引擎

于是华尔街联系你了,他们需要一个新的运行在 iOS 设备上的交易平台。因为是交易平台,所以你需要给交易定义一个
enum


第一步

enum Trades {
case Buy(stock: String, amount: Int, stockPrice: Float)
case Sell(stock: String, amount: Int, stockPrice: Float)
}


同时还会提供如下的 API 给你来进行交易处理。注意销售订单的金额是如何变成负数的,而且你还会被告知股票的价格是不重要的,他们的引擎会在内部选择一个。

/**
- 参数 stock: 股票的名字
- 参数 amount: 金额, 负数表示销售额, 正数表示购买额
*/
func process(stock: String, _ amount: Int) {
print ("\(amount) of \(stock)")
}


下一步就是对交易进行处理。你会发现模式匹配在写这个业务时表现出的强大处理能力:

let aTrade = Trades.Buy(stock: "APPL", amount: 200, stockPrice: 115.5)

switch aTrade {
case .Buy(let stock, let amount, _):
process(stock, amount)
case .Sell(let stock, let amount, _):
process(stock, amount * -1)
}
// 输出 "buy 200 of APPL"


Swift 可以让我们非常方便滴从
enum
中解构/提取出我们真正想要的信息。在这个例子中只有
stock
amount
被解构出来。

真棒,现在你可以去华尔街展示这个极好的交易平台了。然而,现实往往比美好的想象要残酷得多。你以为交易就是你以为的交易么?

你必须根据不同的交易方式计算费用。

机构越小,费用越高

而且,机构越大,优先级越高。

华尔街的人也意识到要处理这些问题你需要新的 API,所以他们给了你下面的两个:

func processSlow(stock: String, _ amount: Int, _ fee: Float) { print("slow") }
func processFast(stock: String, _ amount: Int, _ fee: Float) { print("fast") }


交易类型

于是你回到绘图板重新增加了一个
enum
。交易类型也是每个交易的一部分。

enum TraderType {
case SingleGuy
case Company
}

enum Trades {
case Buy(stock: String, amount: Int, stockPrice: Float, type: TraderType)
case Sell(stock: String, amount: Int, stockPrice: Float, type: TraderType)
}


所以,如何去最好滴实现这一新的机制呢?你可以用一个
if / else
分支来实现购买和销售,但是这会导致代码嵌套以至于很快代码就变的不清晰了——而且谁知道那些华尔街人会不会给你找新的麻烦。所以你应该把它定义为模式匹配的一个新要求:

let aTrade = Trades.Sell(stock: "GOOG", amount: 100, stockPrice: 666.0, type: TraderType.Company)

switch aTrade {
case let .Buy(stock, amount, _, TraderType.SingleGuy):
processSlow(stock, amount, 5.0)
case let .Sell(stock, amount, _, TraderType.SingleGuy):
processSlow(stock, -1 * amount, 5.0)
case let .Buy(stock, amount, _, TraderType.Company):
processFast(stock, amount, 2.0)
case let .Sell(stock, amount, _, TraderType.Company):
processFast(stock, -1 * amount, 2.0)
}


这段代码的优雅之处在于它非常简洁的描述了不同可能的组合。注意我们是如何把
.Buy(let stock, let amount)
修改成
let .Buy(stock, amount)
来让事情更简单一些。这样就可以用更少的语句来像之前一样对
enum
进行解构。

警卫!警卫!呼叫警卫!

于是你再次向你的华尔街用户展示你的开发成果,而他们则又提出了新的问题(你真应该把项目的细节问得更清楚一点)。

交易总额超过 1.000.000$ 的销售订单通常需要更快进行处理,就算是个人客户也得这样。

交易总额小于 1.000$ 的购买订单通常处理更慢。

如果使用传统的
if
语句,这时代码就应该已经有点凌乱了,而
switch
就不会。Swift 为
switch cases
提供了保护机制,这种机制可以让你进一步滴对可能匹配的
case
进行约束。

你只需要对
switch
语句稍作修改就可以满足新的变化。

let aTrade = Trades.Buy(stock: "GOOG", amount: 1000, stockPrice: 666.0, type: TraderType.SingleGuy)

switch aTrade {
case let .Buy(stock, amount, _, TraderType.SingleGuy):
processSlow(stock, amount, 5.0)
case let .Sell(stock, amount, price, TraderType.SingleGuy)
where price*Float(amount) > 1000000:
processFast(stock, -1 * amount, 5.0)
case let .Sell(stock, amount, _, TraderType.SingleGuy):
processSlow(stock, -1 * amount, 5.0)
case let .Buy(stock, amount, price, TraderType.Company)
where price*Float(amount) < 1000:
processSlow(stock, amount, 2.0)
case let .Buy(stock, amount, _, TraderType.Company):
processFast(stock, amount, 2.0)
case let .Sell(stock, amount, _, TraderType.Company):
processFast(stock, -1 * amount, 2.0)
}


上面的代码结构很清晰,阅读起来也相当简单,对复杂情况的封装也很好。

就是这样,我们已经成功滴实现了我们的交易引擎。然而,这个解决方案还是有点繁琐;我们在想是否还有对其进行改进的模式匹配方案。所以,让我们继续深入研究一下模式匹配。

模式匹配进阶

现在我们在实战中已经见过了几种模式。但其语法是什么?还能匹配什么?Swift 将这些模式分为 7 种。我们现在就来认识一下它们。

所有的这些模式不仅能用在
switch
关键词上,而且可以用在
if
guard
for
关键词上。如需了解详情,接着看下面的内容。

1. 通配符模式

通配符模式会忽略需要匹配的值,这种
case
下任何值都是有可能的。这和
let _ = fn()
一样的模式,在这个模式下,
_
表示你将不再使用这个值。有意思的是这个模式可以匹配包括
nil
在内的所有值 1 。如果增加一个
?
,它甚至可以匹配可选值:

let p: String? = nil
switch p {
case _?: print ("Has String")
case nil: print ("No String")
}


就像你在交易例子里面看到的一样,它也允许你忽略需要匹配的
enum
或者
tuples
中无用的数据:

switch (15, "example", 3.14) {
case (_, _, let pi): print ("pi: \(pi)")
}


2. 标示模式

匹配一个具体的值。这个和 Objective-C 的
switch
实现是一样的:

switch 5 {
case 5: print("5")
}


3. 值绑定模式

这种模式和通过
let
或者
var
绑定值到变量中一样。而且仅仅只在一个
switch
语句中实现。因为你之前已经见到过,所以我只给出一个非常简单的例子:

switch (4, 5) {
case let (x, y): print("\(x) \(y)")
}


4. 元组模式

关于元组我已经写了一整篇博文,这篇博文所提供的信息远远比这里多,但是我还是在这里给出一个简短的例子:

let age = 23
let job: String? = "Operator"
let payload: AnyObject = NSDictionary()

switch (age, job, payload) {
case (let age, _?, _ as NSDictionary):
print(age)
default: ()
}


在这里,我们把 3 个值结合放到一个元组中(假想它们是通过调用不同的 API 得到的),然后一口气匹配它们,注意这个模式完成了三件事情:

提取
age


确保存在一个
job
,就算我们不需要它

确保
payload
的类型是
NSDictionary
,尽管我们同样不需要访问它的具体值。

5. 枚举 Case 模式(Enumeration Case Pattern)

就如你在交易例子中所见,模式匹配对 Swift 的
enum
支持得相当棒。这是因为
enum cases
就像密封、不可变且可解构的结构体。这非常像
tuples
,你可以打开正好匹配上的某个单独
case
的内容然后只抽取出你需要的信息2

假想你正在用函数式的风格写一个游戏,然后你需要定义一些实体。你可以使用
structs
但是你的实体的状态很少,你觉得这样有点矫枉过正。

enum Entities {
case Soldier(x: Int, y: Int)
case Tank(x: Int, y: Int)
case Player(x: Int, y: Int)
}


现在你需要实现绘图循环。这里我们只需要 X 和 Y 坐标:

for e in entities() {
switch e {
case let .Soldier(x, y):
drawImage("soldier.png", x, y)
case let .Tank(x, y):
drawImage("tank.png", x, y)
case let .Player(x, y):
drawImage("player.png", x, y)
}
}


6. 类型转换模式

就像名字所表示的一样,这种模式转换或者匹配类型。它有两种不同的关键词:

is
类型:匹配右手边内容的运行时类型(或者类型的子类)。它会做类型转换但是不关注返回值。所以你的
case
块不知道所匹配的类型是什么。

模式
as
类型:和
is
模式做同样的匹配操作,但是如果成功的话会把类型转换到左侧指定的模式中。

下面是这两种关键词的例子:

let a: Any = 5
switch a {
// 这会失败因为它的类型仍然是 `Any`
// 错误: binary operator '+' cannot be applied to operands of type 'Any' and 'Int'
case is Int: print (a + 1)
// 有效并返回 '6'
case let n as Int: print (n + 1)
default: ()
}


注意
is
前没有
pattern
。它直接和
a
做匹配。

7. 表达模式

表达模式非常强大。它可以把
switch
的值和实现了
~=
操作符的表达式进行匹配。而且对于这个操作符有默认的实现,比如对于范围匹配,你可以这样做:

switch 5 {
case 0..10: print("In range 0-10")
}


然而,更有趣的可能是自己重写操作符,然后使你的自定义类型可以匹配。我们假定你想重写之前写的士兵游戏,而且你无论如何都要使用结构体。

struct Soldier {
let hp: Int
let x: Int
let y: Int
}


现在你想轻松滴匹配所有血量为 0 的实体。我们可以像下面一样实现
~=
操作符。

func ~= (pattern: Int, value: Soldier) -> Bool {
return pattern == value.hp
}


现在我们就可以对一个实体做匹配了:

let soldier = Soldier(hp: 99, x: 10, y: 10)
switch soldier {
case 0: print("dead soldier")
default: ()
}


不幸滴是,对元组做全匹配似乎不好使。如果你实现下面的代码,就会出现类型检查错误。

func ~= (pattern: (hp: Int, x: Int, y: Int), value: Soldier) -> Bool {
let (hp, x, y) = pattern
return hp == value.hp && x == value.x && y == value.y
}


一个可能解决上述类似问题的方案是给你的
struct
增加一个
unapply
方法然后再进行匹配:

extension Soldier {
func unapply() -> (Int, Int, Int) {
return (self.hp, self.x, self.y)
}
}

func ~= (p: (Int, Int, Int), t: (Int, Int, Int)) -> Bool {
return p.0 == t.0 && p.1 == t.1 && p.2 == t.2
}

let soldier = Soldier(hp: 99, x: 10, y: 10)
print(soldier.unapply() ~= (99, 10, 10))


但是这相当麻烦而且没有利用好模式匹配背后的大量魔法。

在这篇博文之前的版本中我写过
~=
不适用于协议,但是我错了。我记得我在一个
Playground
中试过。而这个例子(由 reddit 上的 latrodectus 友情提供)是完全可用的:

protocol Entity {
var value: Int {get}
}

struct Tank: Entity {
var value: Int
init(_ value: Int) { self.value = value }
}

struct Peasant: Entity {
var value: Int
init(_ value: Int) { self.value = value }
}

func ~=(pattern: Entity, x: Entity) -> Bool {
return pattern.value == x.value
}

switch Tank(42) {
case Peasant(42): print("Matched") // 匹配成功
default: ()
}


你可以利用
Expression Patterns
做很多事情。如果想要了解更多表达模式的细节,看看这篇由 Austin Zheng 写的超棒博文

现在我们已经讲完了所有可能的
switch
模式。在我们继续讲解之前,还需要讨论最后一件事情。

fallthrough,break 和标签

下面的内容和模式匹配没有直接关系,仅仅是和
switch
关键词有关,所以我就简单说了。默认来说,和 C/C++/Objective-C不一样的是:
switch cases
不会自动进入下一个
case
,这也是为什么 Swift 不需要给每个
case
都写上
break
。你可以选择使用
fallthrough
关键词来实现传统的自动进入下一个
case
的行为。

switch 5 {
case 5:
print("Is 5")
fallthrough
default:
print("Is a number")
}
// 会在命令行输出: "Is 5" "Is a number"


另外,你可以使用
break
来提前跳出
switch
语句。既然不会默认进入下一个
case
,为什么还需要这么做呢?比如你知道在一个
case
中有一个必须的要求是不满足的,这样你就不能继续执行这个
case
了:

let userType = "system"
let userID = 10
switch (userType, userID)  {
case ("system", _):
guard let userData = getSystemUser(userID) else { break }
print("user info: \(userData)")
insertIntoRemoteDB(userData)
default: ()
}
... 更多你需要执行的代码


在这段代码中,当
getSystemUser
返回的结果是
nil
时你不想再继续调用
insertIntoRemoteData
。当然,你可以在这里使用
if let
,但是如果多个这样的情况结合到一起的时候,很快你就会得到一堆可怕丑陋的
if lets
嵌套代码。

但是如果你是在一个
while
循环中执行你的
switch
语句,然后你想跳出循环,而不是
switch
的时候,你需要怎么做呢?对与这种情况, Swift 允许你定义一个
labels
,然后
break
或者
continue
到这个
labels


gameLoop: while true {
switch state() {
case .Waiting: continue gameLoop
case .Done: calculateNextState()
case .GameOver: break gameLoop
}
}


我们已经讨论过
switch
和模式匹配的语法和实现细节。现在,让我们来看一些有趣(多少有点)的真实案例。

真实案例

可选值

对可选值进行解包的方式有很多种,模式匹配就是其中一种。可能到现在这种方法你已经用得非常频繁了,但还是给一个简短的例子吧:

var result: String? = secretMethod()
switch result {
case .None:
println("is nothing")
case let a:
println("\(a) is a value")
}


如果是 Swift 2.0 的话,这会更简单:

var result: String? = secretMethod()
switch result {
case nil:
print("is nothing")
case let a?:
print("\(a) is a value")
}


正如你所见,
result
可以是一个字符串,但是它也可能是
nil
,因为它是
optional
值。通过对
result
执行
switch
。我们可以确定它是
.None
或者是一个确定的值。更进一步,如果他是一个确定的值,我们可以在
a
这种情况下马上把这个值绑定到一个变量。这段代码代码的优美之处在于:变量
result
可能存在的两种状态被非常明显滴区分开来。

类型匹配

做为强类型体系,Swift 通常不会像 Objective-C 那样经常需要运行时类型检查。然而,当你需要与传统的 Objective-C 代码交互时(这还没有更新到简单泛型的反射一文中),那你就经常会碰到需要做类型检查的代码。假想你得到了一个包含
NSString
NSNumber
元素的数组:

let u = NSArray(array: [NSString(string: "String1"), NSNumber(int: 20), NSNumber(int: 40)])


当你遍历这个
NSArray
时,你永远不知道你得到的是什么类型。然而,
switch
语句可以让你很简单滴在这里测试这些类型:

for x in u {
switch x {
case _ as NSString:
print("string")
case _ as NSNumber:
print("number")
default:
print("Unknown types")
}
}


按范围做分级

现在你正在给你当地的高校写分级的 iOS 应用。老师想要输入一个 0 到 100 的数值,然后得到一个相应的等级字符(A-F)。模式匹配现在要来拯救你了:

let aGrade = 84

switch aGrade {
case 90...100: print("A")
case 80...90: print("B")
case 70...80: print("C")
case 60...70: print("D")
case 0...60: print("F")
default:
print("Incorrect Grade")
}


字频率统计

有一系列的数据对,每个数据对代表一个字和它在某段文字中出现的频率。我们的目标就是把那些低于或者高于某个固定阈值的数据对过滤掉,然后只返回剩下的不包含其频率的所有字。

这是我们的字集:

let wordFreqs = [("k", 5), ("a", 7), ("b", 3)]


一个简单的解决方案是使用
map
filter
进行建模:

let res = wordFreqs.filter({ (e) -> Bool in
if e.1 > 3 {
return true
} else {
return false
}
}).map { $0.0 }
print(res)


然而,因为
flatmap
只能返回非空元素,所以这个解决方案还有很大的改进空间。首先最重要的是,我们可以放弃使用
e.1
而利用元组来做适当的解构(你猜对了)。然后我们只需要调用一次
flatmap
,就可以减少先
filter
然后
map
所带来的不必要的性能开销。

let res = wordFreqs.flatMap { (e) -> String? in
switch e {
case let (s, t) where t > 3: return s
default: return nil
}
}
print(res)


遍历目录

假想你需要遍历一个文件树然后查找以下内容:

所有 customer1 和 customer2 创建的 “psd“文件

所有 customer2 创建的 “blend“文件

所有用户创建的 “jpeg“文件

guard let enumerator = NSFileManager.defaultManager().enumeratorAtPath("/customers/2014/")
else { return }

for url in enumerator {
switch (url.pathComponents, url.pathExtension) {

// customer1 和 customer2 创建的 “psd“文件
case (let f, "psd")
where f.contains("customer1")
|| f.contains("customer2"): print(url)

// customer2 创建的 “blend“文件
case (let f, "blend") where f.contains("customer2"): print(url)

// 所有的 “jpeg“文件
case (_, "jpg"): print(url)

default: ()
}
}


注意
contains
在第一个匹配就结束然后就不用遍历完整的路径了。同样,模式匹配的代码非常简洁明了。

Fibonacci

同样,来看一下使用模式匹配实现的 fibonacci 算法有多优美3

func fibonacci(i: Int) -> Int {
switch(i) {
case let n where n <= 0: return 0
case 0, 1: return 1
case let n: return fibonacci(n - 1) + fibonacci(n - 2)
}
}

print(fibonacci(8))


当然,如果是大数的话,程序栈会爆掉。

传统的 API 和值提取

通常情况下,当你从外部源取数据的时候,比如一个库,或者一个 API,它不仅是一种很好的做法,而且通常在解析数据之前需要检查数据的一致性。你需要确保所有的
key
都是存在的、或者数据的类型都正确、或者数组的长度满足要求。如果不这么做就会因为bug(有的
key
没有)而导致 app 崩溃(索引不存在的数组项)。而传统的做法通常是嵌套
if
语句。

假想有 API 返回一条用户信息。但是有两种类型的用户:系统用户——如管理员或者邮政局长——和本地用户——如 “John B“、“Bill Gates“等。因为系统的设计和增长,API 的使用者需要处理一些麻烦的事情:

system
local
用户来自同一个 API 调用。

因为早期版本的数据库没有
department
这个字段,所以这个
key
可能是不存在的,而且早期的雇员从来都不需要填写这个字段。

根据用户被创建的时间,
name
数组可能包含 4 个元素(username,middlename,lastname 和 firstname)或者 2 个元素(full name,username)

age
是代表用户年龄的整型数

我们的系统需要给这个 API 返回的所有系统用户创建用户账号,账号信息只包含如下信息:username 和 department。我们只需要 1980 年以前出生的用户。如果没有指定 department,就指定为 “Corp“。

func legacyAPI(id: Int) -> [String: AnyObject] {
return ["type": "system", "department": "Dark Arts", "age": 57,
"name": ["voldemort", "Tom", "Marvolo", "Riddle"]]
}


我们为给定的约束实现一个模式来进行匹配:

let item = legacyAPI(4)
switch (item["type"], item["department"], item["age"], item["name"]) {
case let (sys as String, dep as String, age as Int, name as [String]) where
age < 1980 &&
sys == "system":
createSystemUser(name.count == 2 ? name.last! : name.first!, dep: dep ?? "Corp")
default:()
}

// 返回 ("voldemort", "Dark Arts")


注意这段代码做了一个很危险的假设:就是如果
name
数组元素的个数不是 2 个的话,那么它一定包含 4 个元素。如果这种假设不成立,我们获得了包含 0 个元素的数组,这段代码就会崩溃。

除了这一点,模式匹配向你展示了它是如何在只有一个
case
的情况下帮助你编写干净的代码和简化值的提取的。

同样来看看我们是怎么写紧跟在
case
之后
let
的,这样一来就不必在每一次赋值的时候都重复写它。

模式和其他关键词

Swift 的文档指出不是所有的模式都可以在
if
for
或者
guard
语句中使用。然而,这个文档似乎不是最新的。所有 7 种模式对这三个关键词都有效。

我为那些感兴趣的人编了一个例子要点,为每个模式和每个关键词都写了一个例子。

你可以在这里查看所有的样例模式

来看一个对三个关键词使用 值绑定元组类型转换模式的简短例子:

// 到吗编译后只是一个关键词的集合。其本身没有任何意义
func valueTupleType(a: (Int, Any)) -> Bool {
// guard case 的例子
guard case let (x, _ as String) = a else { return false}
print(x)

// for case 的例子
for case let (a, _ as String) in [a] {
print(a)
}

// if case 的例子
if case let (x, _ as String) = a {
print("if", x)
}

// switch case example
switch a {
case let (a, _ as String):
print(a)
return true
default: return false
}
}
let u: Any = "a"
let b: Any = 5
print(valueTupleType((5, u)))
print(valueTupleType((5, b)))
// 5, 5, "if 5", 5, true, false


我们可以带着这个想法详细地看一看每一个关键词。

使用 for case

到了 Swift 2.0 后,模式匹配变得更加重要,因为它被扩展到不仅可以支持
switch
,还可以支持其他的关键词。比如,让我们写一个简单的只返回非空元素的数组函数:

func nonnil<T>(array: [T?]) -> [T] {
var result: [T] = []
for case let x? in array {
result.append(x)
}
return result
}

print(nonnil(["a", nil, "b", "c", nil]))


关键词
case
可以被
for
循环使用,就像
switch
中的
case
一样。下面是另外一个例子。还记得我们之前说的游戏么?经过第一次重构之后,现在我们的实体系统看起来是这样的:

enum Entity {
enum EntityType {
case Soldier
case Player
}
case Entry(type: EntityType, x: Int, y: Int, hp: Int)
}


真棒!这可以让我们用更少的代码绘制出所有的项目:

for case let Entity.Entry(t, x, y, _) in gameEntities()
where x > 0 && y > 0 {
drawEntity(t, x, y)
}


我们用一行就解析出了所有必需的属性,然后确保我们不会在 0 一下的范围绘制,最后我们调用渲染方法(
drawEntity
)。

为了知道选手是否在游戏中胜出,我们想要知道是否有至少一个士兵的血量是大于 0 的。

func gameOver() -> Bool {
for case Entity.Entry(.Soldier, _, _, let hp) in gameEntities()
where hp > 0 {return false}
return true
}
print(gameOver())


好的是
Soldier
的匹配是
for
查询的一部分。这感觉有点像
SQL
而不是命令循环编程。同时,这也可以让编译器更清晰滴知道我们的意图,从而就有了打通调度增强这条路的可能性。另外一个很好的体验就是我们不需要完成滴拼写出
Entity.EntityType.Soldier
。就算我们像上面一样只写
.Soldier
,Swift 也能明白我们的意图。

使用 guard case

另外一个支持模式匹配的关键词就是新引入的
guard
关键词。它允许你像
if let
一样把
optionals
绑定到本地范围,而且不需要任何嵌套:

func example(a: String?) {
guard let a = a else { return }
print(a)
}
example("yes")


guard let case
允许你做一些类似模式匹配所介绍的事情。让我们再来看一下士兵的例子。在玩家的血量变满之前,我们需要计算需要增加的血量。士兵不能涨血,所以对于士兵实体而言,我们始终返回 0。

let MAX_HP = 100

func healthHP(entity: Entity) -> Int {
guard case let Entity.Entry(.Player, _, _, hp) = entity
where hp < MAX_HP
else { return 0 }
return MAX_HP - hp
}

print("Soldier", healthHP(Entity.Entry(type: .Soldier, x: 10, y: 10, hp: 79)))
print("Player", healthHP(Entity.Entry(type: .Player, x: 10, y: 10, hp: 57)))

// 输出:
"Soldier 0"
"Player 43"


这是把我们目前讨论的各种机制用到极致的一个例子。

它非常清晰,没有牵扯到任何嵌套

状态的逻辑和初始化是在
func
之前处理的,这样可以提高代码的可读性

非常简洁

这也是
switch
for
的完美结合,可以把复杂的逻辑结构封装成易读的格式。当然,它不会让逻辑变得更容易理解,但是至少会以更清晰的方式展现给你。特别是使用
enums
的时候。

使用 if case

if case
的作用和
guard case
相反。它是一种非常棒滴在分支中打开和匹配数据的方式。结合之前
guard
的例子。很显然,我们需要一个
move
函数,这个函数允许我们说一个实体在朝一个方向移动。因为我们的实体是
enums
,所以我们需要返回一个更新过的实体。

func move(entity: Entity, xd: Int, yd: Int) -> Entity {
if case Entity.Entry(let t, let x, let y, let hp) = entity
where (x + xd) < 1000 &&
(y + yd) < 1000 {
return Entity.Entry(type: t, x: (x + xd), y: (y + yd), hp: hp)
}
return entity
}
print(move(Entity.Entry(type: .Soldier, x: 10, y: 10, hp: 79), xd: 30, yd: 500))
// 输出: Entry(main.Entity.EntityType.Soldier, 40, 510, 79)


限制

一些限制已经在文章中说过,比如有关
Expression Patterns
的问题,看起来它似乎不能匹配
tuples
(那样的话就真的很方便了)。在 Scala 和 Clojure 中,模式匹配在集合上同样可用,所以你可以匹配它的头部、尾部和部分等。4。这在 Swift 中是不支持的(尽管 Austin Zheng 在我之前链接的博客里差不多实现了这一点

另外一种不可用的的情况是(这一点 Scala 同样做得很好)对类或者结构体进行解构。Swift 允许我们定义一个
unapply
方法,这个方法做的事情大体和
init
相反。实现这个方法,然后就可以让类型检查器对类进行匹配。而在 Swift 中,它看起来就像下面一样:

struct Imaginary {
let x: Int
let y: Int
func unapply() -> (Int, Int) {
// 实现这个方法之后,理论上来说实现了解构变量所需的所有细节
return (self.x, self.y)
}
}
// 然后这个就会自动 unapply 然后再进行匹配
guard case let Imaginary(x, y) = anImaginaryObject else { break }


更新

08/21/2015 结合 Reddit 上 foBrowsing 的有用反馈

增加
guard case let


增加简化版的 let 语法(如:
let (x, y)
替代
(let x, let y)


08/22/2015 似乎有一些东西我没测试好。我列举的一些限制实际上是可用的,另外一个 Reddit 上的评论者(latrodectus)提出了一些非常有用的指正。

将之前的修正为:所有的模式对三个关键词都适用,然后增加了一些要点案例

关于协议和表达式模式无效这个限制,其实没有的

增加 “模式可用性“章节

08/24/2015

增加
if case
样例,重命名了一些章节。

修复了一些文本拼写错误。尤其我不小心写道:
_
不能匹配
nil
。那当然是不对的,
_
可以匹配所有的东西。(感谢 obecker

09/18/2015

添加了日语翻译的链接

1.可以把它当做
shell
里面的
*
通配符

2.我不清楚编译器是否在对这点进行了优化,但理论上来说,它应该能计算出所需数据的正确位置,然后忽略
enum
的其他情况并内联这个地址

3.当然,不是 Haskell实现的对手:

fib 0 = 0

fib 1 = 1

fib n = fib (n-1) + fib (n-2)

4.比如:switch [1, 2, 4, 3] {

case [, 2, , 3]:

}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: