可以匹配就匹配咯: 详解 Swift 的模式匹配
怒戳查看最终稿@SwiftGG 在众多 Swift 提供给 Objective-C 程序员使用的新特性中,有个特性把自己伪装成一个无聊的老头,但是却在如何优雅滴解决“鞭尸金字塔“的问题上有着巨大的潜力。很显然我所说的这个特性就是 不过 Swift 中的 这篇博客还有如下语言的版本: 日语 (感谢 M Ono!) 开始咯
// 历史上最坏的一个例子:二进制->十进制的转换
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 Trades {
case Buy(stock: String,amount: Int,stockPrice: Float)
case Sell(stock: String,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,amount * -1)
}
// 输出 "buy 200 of APPL"
Swift 可以让我们非常方便滴从 真棒,现在你可以去华尔街展示这个极好的交易平台了。然而,现实往往比美好的想象要残酷得多。你以为交易就是你以为的交易么?
华尔街的人也意识到要处理这些问题你需要新的 API,所以他们给了你下面的两个: func processSlow(stock: String,_ amount: Int,_ fee: Float) { print("slow") }
func processFast(stock: String,_ fee: Float) { print("fast") }
交易类型于是你回到绘图板重新增加了一个 enum TraderType {
case SingleGuy
case Company
}
enum Trades {
case Buy(stock: String,stockPrice: Float,type: TraderType)
case Sell(stock: String,type: TraderType)
}
所以,如何去最好滴实现这一新的机制呢?你可以用一个 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,5.0)
case let .Sell(stock,-1 * amount,5.0)
case let .Buy(stock,TraderType.Company):
processFast(stock,2.0)
case let .Sell(stock,2.0)
}
这段代码的优雅之处在于它非常简洁的描述了不同可能的组合。注意我们是如何把 警卫!警卫!呼叫警卫!于是你再次向你的华尔街用户展示你的开发成果,而他们则又提出了新的问题(你真应该把项目的细节问得更清楚一点)。
如果使用传统的 你只需要对 let aTrade = Trades.Buy(stock: "GOOG",amount: 1000,type: TraderType.SingleGuy)
switch aTrade {
case let .Buy(stock,5.0)
case let .Sell(stock,price,TraderType.SingleGuy)
where price*Float(amount) > 1000000:
processFast(stock,5.0)
case let .Buy(stock,TraderType.Company)
where price*Float(amount) < 1000:
processSlow(stock,2.0)
case let .Buy(stock,2.0)
case let .Sell(stock,2.0)
}
上面的代码结构很清晰,阅读起来也相当简单,对复杂情况的封装也很好。 就是这样,我们已经成功滴实现了我们的交易引擎。然而,这个解决方案还是有点繁琐;我们在想是否还有对其进行改进的模式匹配方案。所以,让我们继续深入研究一下模式匹配。 模式匹配进阶现在我们在实战中已经见过了几种模式。但其语法是什么?还能匹配什么?Swift 将这些模式分为 7 种。我们现在就来认识一下它们。 所有的这些模式不仅能用在 1. 通配符模式通配符模式会忽略需要匹配的值,这种 let p: String? = nil
switch p {
case _?: print ("Has String")
case nil: print ("No String")
}
就像你在交易例子里面看到的一样,它也允许你忽略需要匹配的 switch (15,"example",3.14) {
case (_,let pi): print ("pi: (pi)")
}
2. 标示模式匹配一个具体的值。这个和 Objective-C 的 switch 5 {
case 5: print("5")
}
3. 值绑定模式这种模式和通过 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 得到的),然后一口气匹配它们,注意这个模式完成了三件事情:
5. 枚举 Case 模式(Enumeration Case Pattern)就如你在交易例子中所见,模式匹配对 Swift 的 假想你正在用函数式的风格写一个游戏,然后你需要定义一些实体。你可以使用 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",y)
case let .Player(x,y):
drawImage("player.png",y)
}
}
6. 类型转换模式就像名字所表示的一样,这种模式转换或者匹配类型。它有两种不同的关键词:
下面是这两种关键词的例子: 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: ()
}
注意 7. 表达模式表达模式非常强大。它可以把 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,y) = pattern
return hp == value.hp && x == value.x && y == value.y
}
一个可能解决上述类似问题的方案是给你的 extension Soldier {
func unapply() -> (Int,Int,Int) {
return (self.hp,self.x,self.y)
}
}
func ~= (p: (Int,Int),t: (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))
但是这相当麻烦而且没有利用好模式匹配背后的大量魔法。 在这篇博文之前的版本中我写过 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: ()
}
你可以利用 现在我们已经讲完了所有可能的 fallthrough,break 和标签下面的内容和模式匹配没有直接关系,仅仅是和 switch 5 {
case 5:
print("Is 5")
fallthrough
default:
print("Is a number")
}
// 会在命令行输出: "Is 5" "Is a number"
另外,你可以使用 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: ()
}
... 更多你需要执行的代码
在这段代码中,当 但是如果你是在一个 gameLoop: while true {
switch state() {
case .Waiting: continue gameLoop
case .Done: calculateNextState()
case .GameOver: break gameLoop
}
}
我们已经讨论过 真实案例可选值对可选值进行解包的方式有很多种,模式匹配就是其中一种。可能到现在这种方法你已经用得非常频繁了,但还是给一个简短的例子吧: 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")
}
正如你所见, 类型匹配做为强类型体系,Swift 通常不会像 Objective-C 那样经常需要运行时类型检查。然而,当你需要与传统的 Objective-C 代码交互时(这还没有更新到简单泛型的反射一文中),那你就经常会碰到需要做类型检查的代码。假想你得到了一个包含 let u = NSArray(array: [NSString(string: "String1"),NSNumber(int: 20),NSNumber(int: 40)])
当你遍历这个 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)]
一个简单的解决方案是使用 let res = wordFreqs.filter({ (e) -> Bool in if e.1 > 3 { return true } else { return false } }).map { $0.0 } print(res)
然而,因为 let res = wordFreqs.flatMap { (e) -> String? in
switch e {
case let (s,t) where t > 3: return s
default: return nil
}
}
print(res)
遍历目录假想你需要遍历一个文件树然后查找以下内容:
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: ()
}
}
注意 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,它不仅是一种很好的做法,而且通常在解析数据之前需要检查数据的一致性。你需要确保所有的 假想有 API 返回一条用户信息。但是有两种类型的用户:系统用户——如管理员或者邮政局长——和本地用户——如 “John B“、“Bill Gates“等。因为系统的设计和增长,API 的使用者需要处理一些麻烦的事情:
我们的系统需要给这个 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")
注意这段代码做了一个很危险的假设:就是如果 除了这一点,模式匹配向你展示了它是如何在只有一个 同样来看看我们是怎么写紧跟在 模式和其他关键词Swift 的文档指出不是所有的模式都可以在 我为那些感兴趣的人编了一个例子要点,为每个模式和每个关键词都写了一个例子。 你可以在这里查看所有的样例模式 来看一个对三个关键词使用 值绑定、元组和类型转换模式的简短例子: // 到吗编译后只是一个关键词的集合。其本身没有任何意义
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",true,false
我们可以带着这个想法详细地看一看每一个关键词。 使用 for case到了 Swift 2.0 后,模式匹配变得更加重要,因为它被扩展到不仅可以支持 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]))
关键词 enum Entity {
enum EntityType {
case Soldier
case Player
}
case Entry(type: EntityType,x: Int,y: Int,hp: Int)
}
真棒!这可以让我们用更少的代码绘制出所有的项目: for case let Entity.Entry(t,y,_) in gameEntities()
where x > 0 && y > 0 {
drawEntity(t,y)
}
我们用一行就解析出了所有必需的属性,然后确保我们不会在 0 一下的范围绘制,最后我们调用渲染方法( 为了知道选手是否在游戏中胜出,我们想要知道是否有至少一个士兵的血量是大于 0 的。 func gameOver() -> Bool {
for case Entity.Entry(.Soldier,let hp) in gameEntities()
where hp > 0 {return false}
return true
}
print(gameOver())
好的是 使用 guard case另外一个支持模式匹配的关键词就是新引入的 func example(a: String?) {
guard let a = a else { return }
print(a)
}
example("yes")
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,y: 10,hp: 79)))
print("Player",healthHP(Entity.Entry(type: .Player,hp: 57)))
// 输出:
"Soldier 0"
"Player 43"
这是把我们目前讨论的各种机制用到极致的一个例子。
这也是 使用 if case
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)
限制一些限制已经在文章中说过,比如有关 另外一种不可用的的情况是(这一点 Scala 同样做得很好)对类或者结构体进行解构。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 的有用反馈
08/22/2015 似乎有一些东西我没测试好。我列举的一些限制实际上是可用的,另外一个 Reddit 上的评论者(latrodectus)提出了一些非常有用的指正。
08/24/2015
09/18/2015
1.可以把它当做 2.我不清楚编译器是否在对这点进行了优化,但理论上来说,它应该能计算出所需数据的正确位置,然后忽略 3.当然,不是 Haskell实现的对手: 4.比如:switch [1,2,4,3] { (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |