通过Playground展示一些编码模式

在Swift中,根据已有的、明确的规则或约定,描述和匹配一组值的方式,我们可将其称之为一种编码模式,比如:

  • 所有的元组在取数据时是从0开始的。
  • 表示数字的范围我们可以使用1...5这种形式。
  • 匹配或判断某些类实例的类型。

该Playground文件需要使用OS X Mavericks或OS X Yosemite beta系统中的Xcode6打开。

Patterns.playground

这个playground文件介绍了一些匹配模式的概念。在Swift中,你可以使用条件语句(比如switch语句)通过简明、易读的方式匹配多个值,这种方式就是一种模式。

注意:
如果你看不到控制台输出界面,你可以通过View > Assistant Editor > Show Assistant Editor选项或使用Option-Command-Return快捷键打开Timeline区域。

匹配元组中的值

下面这个例子向你展示了如何使用匹配模式写出简明、优雅的switch语句。在这个例子中使用了FizzBuzz游戏作为场景进行说明。我们先来简单介绍一下这个游戏,在FizzBuzz游戏中,你从1开始数数,如果你数到的数字能被3整除,那么你就不能说出该数字而要说“Fizz”。如果你数到的数字能被5整除,那么你就要说“Buzz”。如果你数到的数字既能被3整除又能被5整除,那么你就要说“FizzBuzz”。所以一般情况下数数的情形像这样:“1,2,Fizz,4,Buzz…”。那么在这个例子中我们用一个名为fizzBuzz的函数代表该游戏,这个函数有一个参数,代表我们要数的数字,因为我们需要说出“Fizz”、“Buzz”以及“FizzBuzz”,所以返回值为String类型。


func fizzBuzz(number: Int) -> String {
switch (number % 3, number % 5) {
case (0, 0):
// number既能被3整除又能被5整除
return "FizzBuzz!"
case (0, _):
// number能被3整除
return "Fizz!"
case (_, 0):
// number能被5整除
return "Buzz!"
case (_, _):
// number既不能被3整除也不能被5整除
return "\(number)"
}
}

我们通过for循环语句,让fizzBuzz函数参数从1到100执行100次,模拟我们在游戏中从1数到100。然后看看控制台输出的结果。


for i in 1...100 {
println(fizzBuzz(i))
}

fizzBuzz函数中的switch语句中,判断表达式是一个元组,它包含两个成员,这两个成员也是表达式。第一个表达式number % 3,意思是number取3的余数,第二个表达式number % 5,意思是number取5的余数。每一个case语句都对该元组中这两个表达式计算出的值进行匹配判断。

比如,如果number等于15,那么(number % 3, number % 5)的结果就是(0, 0),这代表15既能被3整除又能5整除。这符合switch语句中的第一个case判断,所以返回“FizzBuzz!”。


fizzBuzz(15)

如果number等于6,那么元组的结果为(0, 1),这将符合switch语句中的第二个case判断(0, _),因为下划线在Swift中约定是通配符,它代表任何值。所以将返回“Fizz!”。


fizzBuzz(6)

如果number等于11,那么元组的结果为(2, 1),这将符合switch语句中的第四个case判断(_, _),因为第四个case判断的是既不能被3整除又不能被5整除的情况,所以用两个下划线表示元组中的两个成员。返回结果为“11”。

练习:
fizzBuzz函数根据其他数字返回不同的消息。
再加一种数字的特殊情况,让该函数返回“Bang!”。使用返回“Fizz!”和“Buzz!”相同的模式,比如如果number能被7整除,就返回“Bang!”。别忘了还有“FizzBuzzBang!”这种情况,尽可能将case情况列举全。
如果最后一个case你用default代替case (_ ,_)会发生什么呢?这两种方式都能正确的返回不满足其他case的值么?

枚举和关联值

使用枚举和它的关联值匹配枚举中特定的case场景也是一种匹配模式。下面的例子使用枚举展示了火车的到站时间状态。


enum Status {
case OnTime
case Delayed(minutes: Int)
}

如果火车正点到站,那么它的状态为Status.OnTime,并且没有关联值。当火车晚点,那么它的状态为Status.Delayed(Int),并需要传入一个关联值用于表示火车到底晚了多久。


let goodNews = Status.OnTime
let badNews = Status.Delayed(minutes: 90)

这里有一个名为Train的类,包含一个status属性,默认值为Status.OnTime


class Train {
var status = Status.OnTime
}

你可以使用匹配模式,将Status.Delayed(Int)这种情况的关联值提出来进行判断。下面的代码将Train类进行了扩展,使之遵循Printable协议,添加了一个只读属性description。这个扩展可以很容易的检索出包含火车晚点分钟数的String字符串,并返回。


extension Train: Printable {

var description: String {

switch status {

case .OnTime:
// 满足正点到达的情况,返回“On time”
return "On time"

case .Delayed(let minutes) where 0...5 ~= minutes:
// 将传入的关联值通过“~=”操作符在一定范围内匹配
return "Slight delay of \(minutes) min"

case .Delayed(_):
// 用下划线通配符匹配不在晚点分钟数范围内的情况
return "Delayed"

}

}

}

switch语句中的第一个case用于匹配当火车状态为OnTime时的情况,并返回简单的字符串。

第二个case要稍复杂一些,它创建了一个临时常量minutes来表示传入的关联值,并用where关键字申明一个0到5的范围,判断minutes是否在该范围内,如果在该范围内,那么将这个关联值嵌入字符串返回。

第三个case用于匹配不在晚点范围内的情况。

你现在可以创建一些Train类的实例验证一下。


let trainOne = Train()
let trainTwo = Train()
let trainThree = Train()

trainTwo.status = .Delayed(minutes: 2)
trainThree.status = .Delayed(minutes: 8)

然后使用description属性查看每个Train实例的状态值。


trainOne.description
trainTwo.description
trainThree.description

练习:
改变trainTwotrainThreestatus属性,看看它们的description属性有何变化。
改变Train扩展中switch的最后一个case语句,让它返回包含关联值的字符串,比如“Delayed by 17 min”。
加分项,再增加一种case情况,当关联值大于60时,返回列车晚点几小时几分的字符串。
提示:可以使用>=操作符。

检查和转换子类型

还有一种模式可以让你动态的匹配类的实例。考虑一下下面代码中类的所属关系:


extension Train {
func cleanPassengerCars() -> String {
return "Clean the passenger cars"
}
}

class MaglevTrain: Train {
func referToSpecialist() -> String {
return "Refer the maglev to a specialist"
}
}

let maglev = MaglevTrain()
let train = Train()

有一种简单的类型匹配模式,使用is关键字就可以进行父类与子类之间的匹配和判断。


func trainDescription(train: Train) -> String {
switch train {
case is MaglevTrain:
return "The fastest train on earth."
default:
return "Some other kind of train."
}
}

你可以将刚才创建的Train类和MaglevTrain类实例传入trainDescription函数,看看会有什么结果。


trainDescription(maglev)
trainDescription(train)

练习:
trainDescription函数中的switch语句中再添加一个case,用于匹配判断train是不是Train类型的,然后看看会提示什么错误?为什么呢?
再定义一个Train类的子类SteamTrain,然后在trainDescription函数的switch语句中添加一个case,用于匹配判断train的类型是不是SteamTrain,然后返回适当的字符串描述。将SteamTrain实例传入trainDescription函数,看看是否返回正确的描述。

不过使用is关键字匹配类型只适用于检查子类。如果你想检查对象类型是不是某个类的子类,并且想使用父类的属性或方法时,可以使用as关键字(作用类似类型强制转换中的as)将判断的对象类型转换为父类型,这样在switch语句中就可以同时进行类型检查和类型转换了。使用as关键字时,需要先创建一个临时常量,用于表示需要判断或转换的对象。

下面的代码中有一个名为determineMaintenanceRequirements的函数,在switch语句中判断对象的类型是不是MaglevTrain的子类,如果是MaglevTrain的子类,那么将该对象的类型转换为MaglevTrain。如果转换成功,就可以使用转换后类型的方法。如果失败则返回default的返回值。


func determineMaintenanceRequirements(train: Train) -> String {
switch train {
case let maglev as MaglevTrain:
return maglev.referToSpecialist()
default:
return train.cleanPassengerCars()
}
}

determineMaintenanceRequirements(train)
determineMaintenanceRequirements(maglev)

练习:
SteamTrain类中添加一个名为cleanFirebox的函数,在determineMaintenanceRequirements函数中的switch语句里添加一个case语句,用于判断对象的类型是不是SteamTrain的子类,如果是,将对象的类型转换为SteamTrain类型,并调用SteamTrain类的cleanFirebox函数。然后将SteamTrain类的实例传入determineMaintenanceRequirements函数看看是否能返回正确的描述信息。

原文地址:Patterns Playground