首页 » iOS编程基础:Swift、Xcode和Cocoa入门指南 » iOS编程基础:Swift、Xcode和Cocoa入门指南全文在线阅读

《iOS编程基础:Swift、Xcode和Cocoa入门指南》5.1 流程控制

关灯直达底部

计算机程序都有通过代码语句表示的执行路径。正常来说,这个路径会遵循着一个简单的规则:连续执行每一条语句。不过还有另外的可能。流程控制用于让执行路径跳过某些代码语句,或是重复执行一些代码语句。流程控制使得计算机程序变得“智能”,而不只是执行简单、固定的一系列步骤。通过测试条件(结果为Bool的表达式,因此值为true或false)的真值,程序可以确定如何继续。基于条件测试的流程控制大体上可以分为以下两种类型。

分支

代码被划分为不同的区块,就像树林中分叉的路一样,程序有几种可能进行下去的方式:条件真值用于确定哪一个代码区块会被真正执行。

循环

将代码块划分出来以重复执行:条件真值用于确定代码块是否应该执行,然后是否应该再次执行。每次重复都叫作一次迭代。一般来说,每次迭代时都会改变一些环境特性(比如,变量的值),这样重复就不是一样的了,而是整个任务处理中的连续阶段。

流程控制中的代码块(称为块)是由花括号包围的。这些花括号构成了一个作用域。可以在里面声明新的局部变量,当执行路径离开花括号时,这些局部变量就会自动消亡。对于循环来说,这意味着局部变量在每次迭代时都会创建出来,然后消亡。就像其他作用域那样,花括号中的代码可以看到外部更高层次的作用域。

Swift流程控制相当简单,总的来说类似于C及相关语言。Swift与C存在两种基本的语法差别,这些差别使得Swift变得更加简单和整洁:在Swift中,条件不必放到圆括号中,不过花括号是不能省略的。此外,Swift还添加了一些专门的流程控制特性,帮助你更方便地使用Optional,同时又提供了更为强大的switch语句。

5.1.1 分支

Swift有两种形式的分支:if结构以及switch语句。此外,我还会介绍条件求值,它是if结构的一种紧凑形式。

1.if结构

Swift的if分支结构类似于C,本书之前已经出现过很多if结构的示例。示例5-1总结了if结构的形式。

示例5-1:Swift if结构


if condition {    statements}if condition {    statements} else {    statements}if condition {    statements} else if condition {    statements} else {    statements}  

第3种形式包含了else if,其实它可以根据需要包含多个else if,最后的else块可以省略。

下面的if结构来自于我所编写的一个应用:

自定义嵌套作用域

有时,当知道某个局部变量只需要在几行代码中存在时,你可能会自己定义一个作用域——自定义嵌套作用域,在作用域的开头引入该局部变量,在作用域的结尾该变量会离开,并且其值会自动销毁。

不过,Swift却不允许你使用空的花括号来这么做。在Swift 1.2及之前的版本中,通常的做法是采取一些欺骗的手段,比如,滥用某种形式的流程控制,使之引入合法的嵌套作用域,如if true。Swift 2.0则提供了do结构来实现这个目的:


do {    var myVar = "howdy"    // ... use myVar here ...}// now myVar is out of scope and its value is destroyed  


// okay, we've tapped a tile; there are three casesif self.selectedTile == nil { // no selected tile: select and play this tile    self.selectTile(tile)    self.playTile(tile)} else if self.selectedTile == tile { // selected tile tapped: deselect it    self.deselectAll    self.player?.pause} else { // there was a selected tile, another tile tapped: swap them    self.swap(self.selectedTile, with:tile, check:true, fence:true)}  

2.条件绑定

在Swift中,if后可以紧跟变量声明与赋值,也就是说,其后面可以是let或var,然后是新的局部变量名,后面还可以加上一个冒号以及类型声明,然后是等号和一个值。该语法(叫作条件绑定)实际上是条件展开Optional的简写。赋的值是个Optional(如果不是,则编译器会报错),说明如下。

·如果Optional为nil,那么条件会失败,块也不会执行。

·如果Optional不为nil,那么:

1.Optional会被展开。

2.展开的值会被赋给声明的局部变量。

3.块执行时局部变量会处于作用域中。

因此,条件绑定是将展开的Optional安全地传递给块的一种便捷方式。只有Optional可以展开块才会执行。

条件绑定中的局部变量可以与外部作用域中的已有变量同名。它甚至可以与被展开的Optional同名!没必要创建新的名字,块中的Optional展开值会覆盖原来的Optional,这样就不可能无意中访问到它。

下面是条件绑定的一个示例。如下代码来自于第4章,我展开了NSNotification的userInfo字典,尝试通过“progress”键从字典中获取值,并且只在该值为NSNumber时才继续:


let prog = (n.userInfo?["progress"] as? NSNumber)?.doubleValueif prog != nil {    self.progress = prog!}  

可以通过条件绑定重写上述代码:


if let prog = (n.userInfo?["progress"] as? NSNumber)?.doubleValue {    self.progress = prog}  

还可以对条件绑定进行嵌套。为了说明这一点,我要重写上一个示例,对链中的每个Optional使用单独的条件绑定:


if let ui = n.userInfo {    if let prog : AnyObject = ui["progress"] {        if let prog = prog as? NSNumber {            self.progress = prog.doubleValue        }    }}  

结果更为冗长,嵌套层次也更深(Swift程序员管这叫作“末日金字塔”),不过我却认为其可读性更好,因为其结构能很好地反映出连续的测试过程。为了避免缩进,可以将连续的条件绑定放到一个列表中,中间通过逗号分隔:


if let ui = n.userInfo, prog = ui["progress"] as? NSNumber {    self.progress = prog.doubleValue}  

列表中的绑定甚至可以后跟一个where子句,将另一个条件放到一行当中。整个列表可以从一个条件开始,位于单词let或var前。如下示例来自于我曾编写过的代码(第11章将会对此进行介绍)。“末日金字塔”包含4个嵌套条件:


override func observeValueForKeyPath(keyPath: String?,    ofObject object: AnyObject?, change: [String : AnyObject]?,    context: UnsafeMutablePointer<>) {        if keyPath == "readyForDisplay" {            if let obj = object as? AVPlayerViewController {                if let ok = change?[NSKeyValueChangeNewKey] as? Bool {                    if ok {                        // ...                    }                }            }        }}  

可以将这4个条件组合放到单个列表中:


override func observeValueForKeyPath(keyPath: String?,    ofObject object: AnyObject?, change: [String : AnyObject]?,    context: UnsafeMutablePointer<>) {        if keyPath == "readyForDisplay",        let obj = object as? AVPlayerViewController,        let ok = change?[NSKeyValueChangeNewKey] as? Bool where ok {            // ...        }}  

不过,至于第2个版本是否更清晰可读则是个见仁见智的问题了。

在Swift 2.0中,你可以通过一系列guard语句来表示这个条件链;我觉得下面这种方式是最好的:


override func observeValueForKeyPath(keyPath: String?,    ofObject object: AnyObject?, change: [String : AnyObject]?,    context: UnsafeMutablePointer<>) {        guard keyPath == "readyForDisplay" else {return}        guard let obj = object as? AVPlayerViewController else {return}        guard let ok = change?[NSKeyValueChangeNewKey] as? Bool else {return}        guard ok else {return}        // ...}  

3.Switch语句

Switch语句是一种更为简洁的if...else if...else结构编写方式。在C及Objective-C中,switch语句有个隐藏的陷阱;Swift消除了这个陷阱,并增加了功能与灵活性。这样,switch语句在Swift中得到了广泛的应用(但在我所编写的Objective-C代码中用得却很少)。

在switch语句中,条件位于不同可能值与单个值的比较(叫作标记)中,这叫作case。case比较是按照顺序执行的。如果某个case比较成功,那么case的代码就会执行,整个switch语句将会退出。示例5-2展示了其模式,根据需要可以有多个case,default case则可以省略(有一些限制,稍后将会介绍)。

示例5-2:Swift switch语句


switch tag {case pattern1:    statementscase pattern2:    statementsdefault:    statements}  

下面是个实际的示例:


switch i {case 1:    print("You have 1 thingy!")case 2:    print("You have 2 thingies!")default:    print("You have /(i) thingies!")}  

在上述代码中,变量i作为标记。i的值首先会与1进行比较。如果i为1,那么该case的代码就会执行,然后switch语句退出。如果i不为1,那么它会与2进行比较。如果i为2,那么该case的代码就会执行,然后switch语句退出。如果i值与所有case值都不相等,那么default case的代码就会执行。

在Swift中,switch语句必须是完备的。这意味着标记的每个可能值都必须要被case覆盖到。如果违背了这一原则,编译器就会报错。如果一个值只有有限的可能,这个原则就会显得很直观;这通常来说是枚举,它本身只有为数不多的case作为可能值。不过在上述示例中,标记是个Int,而Int的可能值是很大的,因此case也会有很多。这样就必须要有一个扫尾的case,不过你不必显式提供。常见的扫尾case是使用一个default case。

每个case的代码都可以有多行,不必像上述示例那样只有单行代码。不过,每个case至少要包含一行代码;Swift switch case不允许没有代码。case代码的第1行(也只有这行)可以与case位于同一行;这样就可以像下面这样改写上述示例了:


switch i {case 1: print("You have 1 thingy!")case 2: print("You have 2 thingies!")default: print("You have /(i) thingies!")}  

最少的单行case代码只包含关键字break;在这种情况下,break表示一个占位符,什么都不会做。“很多时候,switch语句会包含一个default(或其他扫尾case),它只包含了关键字break”;这样就可以穷尽标记的所有可能值,不过如果值与哪一个case都不匹配,那就什么都不会做。

现在来看看标记值与case值的比较。在上述示例中,这种比较类似于相等比较(==);不过还有其他情况存在。在Swift中,case值实际上是一个叫作模式的特殊表达式,该模式会通过“隐秘的模式匹配运算符~=”与标记值进行比较。你对构建模式的语法认识越深刻,case值与switch语句就会越强大。

模式还可以包含一个下划线(_)来表示所有其他值。实际上,下划线case是“扫尾case”的一种替代形式:


switch i {case 1:    print("You have 1 thingy!")case _:    print("You have many thingies!")}  

模式可以包含局部变量名的声明(无条件绑定)来表示所有值,并将实际值作为该局部变量的值。这实际上是“扫尾case”的另一种替代方案:


switch i {case 1:    print("You have 1 thingy!")case let n:    print("You have /(n) thingies!")}  

如果标记是个Comparable,那么case还可以包含Range;比较时会向Range发送contains消息:


switch i {case 1:    print("You have 1 thingy!")case 2...10:    print("You have /(i) thingies!")default:    print("You have more thingies than I can count!")}  

如果标记是个Optional,那么case可以将其与nil进行比较。这样,安全展开Optional的一种方式就是先将其与nil进行比较,并在随后的case中将其展开,因为如果nil比较通过,那么流程永远也不会进入到展开case。在如下示例中,i是个包装了Int的Optional:


switch i {case nil: breakdefault:    switch i! {    case 1:        print("You have 1 thingy!")    case let n:        print("You have /(n) thingies!")    }}  

不过,这看起来有点笨拙,因此Swift 2.0提供了一个新的语法:将?追加到case模式上可以安全地展开一个Optional标记。这样,我们就可以像下面这样重写该示例:


switch i {case 1?:    print("You have 1 thingy!")case let n?:    print("You have /(n) thingies!")case nil: break}  

如果标记是个Bool值,那么case就可以将其与条件进行比较了。通过巧妙的使用,你可以通过case测试任何条件:将true作为标记!这样,switch语句就变成了扩展的if...else if结构的替代者。如下示例来自于我所编写的代码,我本可以使用if...else if,但每个case只有一行代码,因此使用switch语句会更加整洁一些:


func positionForBar(bar: UIBarPositioning) -> UIBarPosition {    switch true {    case bar === self.navbar:  return .TopAttached    case bar === self.toolbar: return .Bottom    default:                   return .Any    }}  

模式还可以包含where子句来添加条件,从而限制case的真值。它常常会与绑定搭配使用,但这并非强制要求;条件可以引用绑定中声明的变量:


switch i {case let j where j < 0:    print("i is negative")case let j where j > 0:    print("i is positive")case 0:    print("i is 0")default:break}  

模式可以通过is运算符来判断标记的类型。在如下示例中,假设有个Dog类及其NoisyDog子类,d的类型为Dog:


switch d {case is NoisyDog:    print("You have a noisy dog!")case _:    print("You have a dog.")}  

模式可以通过as(不是as?)运算符进行类型转换。一般来说,你会将其与声明局部变量的绑定搭配使用;虽然使用了无条件的as,但值的类型转换却是有条件的,如果转换成功,那么局部变量就会将转换后的值带入case代码中。假设Dog实现了bark,NoisyDog实现了beQuiet:


switch d {case let nd as NoisyDog:    nd.beQuietcase let d:    d.bark}  

在与特定的值进行比较时,你还可以使用as(不是as!)运算符根据情况对标记进行向下类型转换(可能还会展开);在如下示例中,i可能是个AnyObject,也可能是个包装了AnyObject的Optional:


switch i {case 0 as Int:    print("It is 0")default:break}  

你可以将标记表示为元组,同时将相应的比较也包装到元组中,这样就可以同时进行多个比较了。只有当比较元组与相应的标记元组中的每一项比较都通过,这个case才算通过。在如下示例中,我们从一个类型为[String:AnyObject]的字典d开始。借助元组,我们可以安全地抽取并转换两个值:


switch (d["size"], d["desc"]) {case let (size as Int, desc as String):    print("You have size /(size) and it is /(desc)")default:break}  

如果标记是个枚举,那么case就可以是枚举的case。这样,switch语句就成为处理枚举的绝佳方式,下面是枚举声明:


enum Filter {    case Albums    case Playlists    case Podcasts    case Books}  

下面是个switch语句,其中的标记type是个Filter:


switch type {case .Albums:    print("Albums")case .Playlists:    print("Playlists")case .Podcasts:    print("Podcasts")case .Books:    print("Books")}  

这里不需要“扫尾”case,因为代码已经穷尽了所有case。(在该示例中,case名前的点是必不可少的。不过,如果代码位于枚举声明中,那么点就可以省略。)

Switch语句提供了从枚举case中抽取出关联值的方式。回忆一下第4章介绍的这个枚举:


enum Error {    case Number(Int)    case Message(String)    case Fatal}  

要想从Error中抽取出错误号(其case是.Number),或从Error中抽取出消息字符串(其case是.Message),我可以使用switch语句。回忆一下,关联值实际上是个元组。匹配case名后的模式元组会应用到关联值上。如果模式是个绑定变量,那么它会捕获到关联值。let(或var)可以位于圆括号中,或在case关键字后;如下代码演示了这两种情况:


switch err {case .Number(let theNumber):    print("It is a .Number: /(theNumber)")case let .Message(theMessage):    print("It is a .Message: /(theMessage)")case .Fatal:    print("It is a .Fatal")}  

如果let(或var)位于case关键字之后,那就可以添加一个where子句:


switch err {case let .Number(n) where n > 0:    print("It's a positive error number /(n)")case let .Number(n) where n < 0:    print("It's a negative error number /(n)")case .Number(0):    print("It's a zero error number")default:break}  

如果不想抽取出错误号,只想进行匹配,那就可以在圆括号中使用另外一种模式:


switch err {case .Number(1..<Int.max):    print("It's a positive error number")case .Number(Int.min...(-1)):    print("It's a negative error number")case .Number(0):    print("It's a zero error number")default:break}  

该模式提供了另外一种处理Optional标记的方式。正如第4章所述,Optional实际上是个枚举。它有两种case,分别是.None与.Some,其中被包装的值是与.Some case相关联的值。不过现在我们知道了该如何提取出相关联的值!这样,我们就可以再次重写之前的那个示例,其中i是个包装了Int的Optional:


switch i {case .None: breakcase .Some(1):    print("You have 1 thingy!")case .Some(let n):    print("You have /(n) thingies!")}  

在Swift 2.0中,我们可以通过轻量级的if case结构在条件中使用与switch语句的case相同模式的语法。Switch case模式类似于之前介绍的标记,if case模式后面则会跟着一个等号和标记。实际上,这对于通过单个条件绑定来从枚举中提取出关联值是非常有用的(下面的err是Error枚举)。


if case let .Number(n) = err {    print("The error number is /(n)")}  

甚至还可以在switch case中附加一个where子句:


if case let .Number(n) = err where n < 0 {    print("The negative error number is /(n)")}  

要想将case测试组合起来(使用隐式的逻辑或),可以用逗号将其分隔开:


switch i {case 1,3,5,7,9:    print("You have a small odd number of thingies.")case 2,4,6,8,10:    print("You have a small even number of thingies.")default:    print("You have too many thingies for me to count.")}  

在该示例中,i是个AnyObject:


switch i {case is Int, is Double:    print("It's some kind of number.")default:    print("I don't know what it is.")}  

不过,你不能使用逗号组合声明绑定变量的模式,因为在这种情况下,对变量的赋值是不明确的。

组合case的另一种方式是通过fallthrough语句从一个case落到下一个case上。虽然一个case执行完一些代码后落到下一个case上是合法的,但很多时候一个case只会包含一个fallthrough语句:


switch pep {case "Manny": fallthroughcase "Moe": fallthroughcase "Jack":    print("/(pep) is a Pep boy")default:    print("I don't know who /(pep) is")}  

注意,fallthrough会跳过下一个case的测试;它会直接开始执行下一个case的代码。因此,下一个case不能声明任何绑定变量,因为无法对变量赋值。

4.条件求值

在确定使用什么值时会出现一个有意思的问题,比如,将什么值赋给变量。这看起来像是分支结构的用武之地。当然,你可以先声明变量但不对其初始化,并在随后的分支结构中设置其值。不过,使用分支结构作为变量值会更好一些。比如,下面是个变量赋值语句,其中等号后面会直接跟着一个分支结构(代码无法编译通过):


let title = switch type { // compile errorcase .Albums:    "Albums"case .Playlists:    "Playlists"case .Podcasts:    "Podcasts"case .Books:    "Books"}  

有些语言允许这么做,但Swift不行。不过可以采取一个易于实现的变通办法:使用定义与调用匿名函数:


let title : String = {    switch type {    case .Albums:        return "Albums"    case .Playlists:        return "Playlists"    case .Podcasts:        return "Podcasts"    case .Books:        return "Books"    }}  

有时,值通过一个二路条件才能确定下来,Swift提供了类似于C语言的三元运算符(:?)。其模式如下所示:


condition ? exp1 : exp2  

如果条件为true,那么表达式“exp1”会求值并将值作为结果;否则,表达式“exp2”会求值并将值作为结果。这样,在赋值时就可以使用三元运算符了,模式如下所示:


let myVariable = condition ? exp1 : exp2  

myVariable的初始值取决于条件的真值情况。我在代码中大量使用了三元运算符,比如:


cell.accessoryType =    ix.row == self.currow ? .Checkmark : .DisclosureIndicator  

上下文不一定非得是个赋值;如下示例会确定将什么值作为函数实参传递进去:


CGContextSetFillColorWithColor(    context, self.hilite ? purple.CGColor : beige.CGColor)  

在现代Objective-C所使用的C版本中,有一种压缩形式的三元运算符,它可以将值与nil进行比较。如果为nil,那么你可以提供一个替代值。如果不为nil,那么使用的就是被测试的值本身。在Swift中,类似的操作涉及对Optional的判断:如果被测试的Optional为nil,那就会使用替代值;否则会展开Optional,并使用被包装的值。Swift提供了这样一个运算符:??运算符(叫作nil合并运算符)。

回忆一下第4章的示例,其中arr是个Swift的Optional String数组,我对其进行了转换,使得转换后的结果可以以NSArray的形式传递给Objective-C:


let arr2 : [AnyObject] =    arr.map{if $0 == nil {return NSNull} else {return $0!}}  

可以通过三元运算符以更加整洁的方式完成相同的事情:


let arr2 = arr.map { $0 != nil ? $0! : NSNull }  

nil合并运算符甚至更加整洁:


let arr2 = arr.map{ $0 ?? NSNull }  

可以将使用??的表达式链接起来:


let someNumber = i1 as? Int ?? i2 as? Int ?? 0  

上述代码尝试将i1类型转换为Int并使用该Int。如果失败,那么它会尝试将i2类型转换为Int并使用该Int。如果这也失败,那么它就会放弃并使用0。

5.1.2 循环

循环的目的在于重复一个代码块的执行,并且在每次迭代时会有一些差别。这种差别通常还作为循环停止的信号。Swift提供了两种基本的循环结构:while循环与for循环。

1.while循环

while循环有两种形式,如示例5-3所示。

示例5-3:Swift while循环


while condition {    statements}repeat {    statements} while condition  

这两种形式之间的主要差别在于测试的次数。在第2种形式中,代码块执行后才会测试条件,这意味着代码块至少会被执行一次。

一般来说,块中的代码会修改一些东西,这会影响环境与条件,最终让循环结束。如下典型示例来自于我之前所编写的代码(movenda是个数组):


while self.movenda.count > 0 {    let p = self.movenda.removeLast    // ...}  

每次迭代都会从movenda中删除一个元素,最终其数量会变为0,循环也将终止;接下来,执行会进入到右花括号的下一行代码。

while循环第一种形式的条件可以是个Optional的条件绑定。这提供了一种紧凑且安全的方式来展开Optional,然后循环,直到Optional为nil;包含了展开的Optional的局部变量位于花括号的作用域中。这样,我们就能以更加简洁的方式改写代码:


while let p = self.movenda.popLast {    // ...}  

在我的代码中,while循环的另一种常见用法是沿着层次结构向上或向下遍历。在如下示例中,首先从表视图单元的一个子视图(textField)开始,我想知道它属于哪个表视图单元。因此,我会沿着视图层次向上遍历,比较每个父视图,直到遇到了表视图单元:


var v : UIView = textFieldrepeat { v = v.superview! } while !(v is UITableViewCell)  

上述代码执行完毕后,v就是我们要找的表视图单元。不过,上述代码有些危险:如果在到达视图层次结构的最顶层时没有遇到UITableViewCell(也就是说superview为nil的视图),那么程序就会崩溃。下面以一种安全的方式来编写同样的代码:


var v : UIView = textFieldwhile let vv = v.superview where !(vv is UITableViewCell) {v = vv}if let c = v.superview as? UITableViewCell { // ...  

类似于if case结构,在while case中也可以使用switch case模式。在下面这个想象出来的示例中有一个由各种Error枚举所构成的数组:


let arr : [Error] = [    .Message("ouch"), .Message("yipes"), .Number(10), .Number(-1), .Fatal]  

我们可以从数组起始位置开始提取出与.Message关联的字符串值,如以下代码所示:


var i = 0while case let .Message(message) = arr[i++] {    print(message) // "ouch", then "yipes"; then the loop stops}  

2.for循环

Swift for循环有两种形式,如示例5-4所示。

示例5-4:Swift for循环


for variable in sequence {    statements}for before-all; condition; after-each {    statements}  

第1种形式(for...in结构)类似于Objective-C的for...in结构。在Objective-C中,如果一个类遵循了NSFastEnumeration协议,那么就可以使用该for循环语法。在Swift中,如果一个类型使用了SequenceType协议,那么就可以使用该for循环语法。

在for...in结构中,变量会在每次迭代中隐式通过let进行声明;因此在默认情况下它是不可变的。(如果需要对块中的变量进行赋值,那么请写成for var。)该变量对于块来说也是局部变量。在每次迭代中,序列中连续的元素用于初始化变量,它位于块作用域中。你会经常使用这种for循环形式,因为在Swift中,创建序列是非常轻松的事情。在C中,遍历数字1到15的方式是使用第2种形式的for循环,在Swift中当然也可以这么做:


for var i = 1; i < 6; i++ {    print(i)}  

不过在Swift中,你可以很方便地创建数字1到5的序列(是个Range),一般来说你会这么做:


for i in 1...5 {    print(i)}  

SequenceType有个generate方法,它会生成一个“迭代器”对象,这个迭代器对象本身有个mutating的next方法,该方法会返回序列中的下一个对象,并被包装到Optional中,如果没有下一个对象,那么它会返回nil。在底层,for...in实际上是一种while循环:


var g = (1...5).generatewhile let i = g.next {    print(i)}  

有时,你会发现像上面这样显式使用while循环会使得循环更易于控制和定制。

序列通常是个已经存在的值。它可以是字符序列,这样变量值就是连续的Character。它可以是Array,这样变量值就是连续的数组元素。它可以是字典,这样变量值就是键值对元组,你可以将变量表示为两个名字的元组,从而可以捕获到它们。之前的章节中已经介绍了一些示例。

正如第4章所述,你可能会遇到来自于Objective-C的数组,其元素需要从AnyObject进行向下类型转换。我们常常会将其作为序列规范的一部分:


let p = Pepfor boy in p.boys as! [String] {    // ...}  

序列的enumerate方法会生成一个元组序列,并在原始序列的每个元素前加上其索引号:


for (i,v) in self.tiles.enumerate {    v.center = self.centers[i]}  

如果想要跳过序列的某些值,在Swift 2.0中可以附加一个where子句:


for i in 0...10 where i % 2 == 0 {    print(i) // 0, 2, 4, 6, 8, 10}  

就像if case与while case一样,还有一个for case。回到之前Error枚举数组那个示例:


let arr : [Error] = [    .Message("ouch"), .Message("yipes"), .Number(10), .Number(-1), .Fatal]  

遍历整个数组,只提取出与.Number关联的值:


for case let .Number(i) in arr {    print(i) // 10, then -1}  

序列还有实例方法,如map、filter和reverse;如下示例倒序输出了偶数数字:


let range = (0...10).reverse.filter{$0 % 2 == 0}for i in range {    print(i) // 10, 8, 6, 4, 2, 0}  

另一种方式是通过调用stride方法来生成序列。它是Strideable协议的一个实例方法,并且被数字类型与可以增加及减少的所有类型所使用。它拥有两种形式:

·stride(through:by:)

·stride(to:by:)

使用哪种形式取决于你是否希望序列包含最终值。by:参数可以是负数:


for 10.stride(through: 0, by: -2) {    print(i) // 10, 8, 6, 4, 2, 0}  

可以通过全局的zip函数同时遍历两个序列,它接收两个序列,并生成一个Zip2结构体,其本身也是个序列。每次遍历Zip2得到的值都是原来的两个序列中相应元素所构成的元组;如果原来的一个序列比另一个长,那么额外的元素会被忽略:


let arr1 = ["CA", "MD", "NY", "AZ"]let arr2 = ["California", "Maryland", "New York"]var d = [String:String]for (s1,s2) in zip(arr1,arr2) {    d[s1] = s2} // now d is ["MD": "Maryland", "NY": "New York", "CA": "California"]  

第2种形式的for循环来源于C的循环(参见示例5-4),它主要用于增加或减少计数器值。before-all语句会在进入for循环时执行一次,它通常用于计数器的初始化。接下来会测试条件,如果为true,那么代码块就会执行;条件通常用来测试计数器是否到达了某个限值。接下来会执行after-each语句,它通常用于增加或减少计数器值;接下来会再次测试条件。因此,要想使用整数值1、2、3、4与5执行代码块,这种形式的for循环的标准做法如下所示:


var i : Intfor i = 1; i < 6; i++ {    print(i)}  

要想将计数器的作用域限制到花括号中,请在before-all语句中声明它:


for var i = 1; i < 6; i++ {    print(i)}  

不过,没有规则说这种形式的for循环就一定是与计数或值增加相关的。回忆一下之前介绍的关于while循环的示例,我们遍历了视图层次,查找某个表视图单元:


var v : UIView = textFieldrepeat { v = v.superview! } while !(v is UITableViewCell)  

下面是另一种做法,使用一个for循环,其代码块是空的:


var v : UIViewfor v = textField; !(v is UITableViewCell); v = v.superview! {}  

就像C一样,声明中的语句(用分号分隔)可以包含多个代码声明(用逗号分隔)。这是一种表明意图的便捷、优雅的方式。下面这个示例来自于我之前编写的代码,我在before-all语句中声明了两个变量,然后在after-each语句中修改了它们;当然,完成这个任务不止这一种方法,不过这种方式看起来最简洁:


var values = [0.0]for (var i = 20, direction = 1.0; i < 60; i += 5, direction *= -1) {    values.append( direction * M_PI / Double(i) )}  

5.1.3 跳转

虽然分支与循环构成了代码执行的大多数决策流程,但有时它们还不足以表达出所需的逻辑。我们偶尔还需要完全终止代码的执行,并跳转到其他地方。

从一个地方跳转到另一个地方最常见的方式是使用goto命令,这在早期编程语言中是非常普遍的,不过现在却被认为是“有害的”。Swift并未提供goto命令,不过它提供了一些跳转控制方式,在实际情况下可以涵盖所有的情况。Swift的跳转模式都是以从当前代码流中提前退出的形式而存在的。

你已经对最重要的一种提前退出模式很熟悉了,那就是return,它会立即终止当前的函数,并在函数调用处继续执行。这样,我们可以认为return就是一种跳转形式。

1.短路与标签

Swift提供了几种方式来短路分支与循环结构流:

fallthrough

Switch case中的fallthrough语句会终止当前case代码的执行,并立刻开始下一个case代码的执行。必须要有下一个case,否则编译器会报错。

continue

循环结构中的continue语句会终止当前迭代的执行,然后继续下一次迭代:

·在while循环中,continue表示立刻执行条件测试。

·在第1种for循环中(for...in),continue表示如果有下一次迭代,那么它会立刻进入下一次迭代中。

·在第2种for循环中(C语言中的for循环),continue表示立刻执行after-each语句和条件测试。

break

break语句会终止当前结构:

·在循环中,break会完全终止循环。

·在switch case代码中,break会终止整个switch结构。

如果结构是嵌套的,那么你可能就需要指定想要continue或break哪一个结构。因此,Swift支持在do块、if结构、switch语句、while循环或for循环前放置一个标签。标签可以是任何名字,后跟一个冒号。接下来可以在任意深度结构中的continue或break后面加上标签名,指定你所引用的结构。

如下示例用于说明语法。首先,我不使用标签嵌套了两个for循环:


for i in 1...5 {    for j in 1...5 {        print("/(i), /(j);")        break    }}// 1, 1; 2, 1; 3, 1; 4, 1; 5, 1;  

从输出可以看到,上述代码会在一次迭代后终止内部循环,而外部循环则会正常执行5次迭代。但如果想要终止整个嵌套结构该怎么办呢?解决办法就是使用标签:


outer: for i in 1...5 {    for j in 1...5 {        print("/(i), /(j);")        break outer    }}// 1, 1;  

在Swift 2.0中,你可以在单词if前放置一个标签,还可以在if或else块的代码中将break与标签名搭配使用;与之类似,你可以在单词do之前放置一个标签,并在do块中将break与标签名搭配使用。借助这些举措,我们可以认为Swift的短路功能是特性完备的。

2.抛出与捕获错误

有时会出现无法达成一致的情况:我们所进入的整个操作失败了。接下来就需要终止当前作用域,这可能是当前函数,甚至还可能是调用它的函数等,然后退出到能接收到这个失败的地方,并通过其他方式继续进行。

出于这个目的,Swift 2.0提供了一种抛出与捕获错误的机制。为了保持其一以贯之的安全性与清晰性,Swift对这种机制的使用施加了一些严格的条件,编译器会确保你遵守了这些条件。

从这个意义上来说,错误是一种消息,指出了出错的地方。作为错误处理过程的一部分,该消息会沿着作用域与函数调用向上传递,如果需要,从失败中恢复的代码会读取该消息,然后决定该如何继续。在Swift中,错误一定是使用了ErrorType协议的类型的对象,它只有两点要求:一个String类型的_domain属性,以及一个Int类型的_code属性。实际上,它指的是如下两者之一:

NSError

NSError是Cocoa中用于与问题本质通信的类。如果对Cocoa方法的调用导致了失败,那么Cocoa会向你发送一个NSError实例。还可以通过调用其指定初始化器init(domain:code:userInfo:)来创建自己的NSError实例。

使用了ErrorType的Swift类型

如果一个类型使用了ErrorType协议,那么就可以将其作为错误对象;在背后,协议的要求会神奇般地得到满足。一般来说,该类型是个枚举,它会通过其case来与消息通信:不同的case会区分不同类型的失败,也许一些原始值或关联值还会持有更多的信息。

有两个错误机制阶段需要考虑:抛出错误与捕获错误。抛出错误会终止当前的执行路径,并将错误对象传递给错误处理机制。捕获错误会接收错误对象并对其进行响应,在捕获点后执行路径会继续。实际上,我们会从抛出点跳转到捕获点。

要想抛出错误,请使用关键字throw并后跟错误对象。这会导致当前代码块终止执行,同时错误处理机制会介入。为了确保throw命令的使用能够做到前后一致,Swift应用了一条规则,你只能在如下两个地方使用throw:

在do...catch结构的do块中

do...catch结构至少包含了两个块,即do块与catch块。该结构的要点在于catch块可以接收do块所抛出的任何错误。因此,我们就可以前后一致地处理这种错误,错误可以被捕获到。稍后将会更加详尽地介绍do...catch结构。

在标记了throws的函数中

如果不在do...catch结构的do块中抛出错误,或在do块中抛出了错误,但catch块没有将其捕获,那么错误消息就会跳出当前函数。这样就需要依赖于其他函数了,即调用该函数的函数,或更外层的函数,以此类推一直沿着调用栈向上,通过这种方式来捕获错误。要想告知任何调用者(以及编译器)错误发生了,函数需要在其声明中加上关键字throws。

要想捕获错误,请使用do...catch结构。从do块中抛出的错误可以被与之相伴的catch块所捕获。do...catch结构的模式类似于示例5-5。

示例5-5:Swift do...catch结构


do {     statements // a throw can happen here} catch errortype {     statements} catch {     statements}  

一个do块后面可以跟着多个catch块。Catch块类似于switch语句中的case,通常也都具有同样的逻辑:首先,你会有专门的catch块,其中每一个都用于处理可能会出现的一部分错误;最后会有一个一般性的catch块,它作为默认值,处理其他专门的catch块所没有捕获到的错误。

实际上,catch块所用的捕获指定错误的语法就是switch语句中的case所用的模式语法!可以将其看作一个switch语句,标记就是错误对象。接下来,错误对象与特定catch块的匹配就好像使用的是case而非catch一样。通常,如果ErrorType是个枚举,那么专门的catch块至少会声明它所捕获到的枚举,也许还有该枚举的case;它可以通过绑定来捕获到该枚举或与其关联的类型;还可以通过where子句来进一步限定可能性。

为了说明问题,我首先定义两个错误:


enum MyFirstError : ErrorType {    case FirstMinorMistake    case FirstMajorMistake    case FirstFatalMistake}enum MySecondError : ErrorType {    case SecondMinorMistake(i:Int)    case SecondMajorMistake(s:String)    case SecondFatalMistake}  

下面的do...catch结构用于说明在不同的catch块中捕获不同错误的方式:


do {    // throw can happen here} catch MyFirstError.FirstMinorMistake {    // catches MyFirstError.FirstMinorMistake} catch let err as MyFirstError {    // catches all other cases of MyFirstError} catch MySecondError.SecondMinorMistake(let i) where i < 0 {    // catches e.g. MySecondError.SecondMinorMistake(i:-3)} catch {    // catches everything else}  

在使用了伴随模式的catch块中,你可以在模式中决定捕获关于错误的何种信息。比如,如果希望将错误本身当作变量传递到catch块中,那就需要在模式中进行绑定。在没有使用伴随模式的catch块中,错误对象会以一个名为error的变量形式进入块中。

如果函数中的代码使用了throw,同时代码又不处于拥有“收尾”catch块的do块中,那么该函数本身就要标记为throws,因为如果没有捕获到每一个可能的错误,同时代码又抛出了错误,那么该错误就会离开所在的函数。语法要求关键字throws要紧跟参数列表之后(如果有箭头运算符,则还要位于它之前)。比如:


enum NotLongEnough : ErrorType {    case ISaidLongIMeantLong}func giveMeALongString(s:String) throws {    if s.characters.count < 5 {        throw NotLongEnough.ISaidLongIMeantLong    }    print("thanks for the string")}  

向函数声明添加的throws创建了一个新的函数类型。giveMeALongString的类型不是(String)->(),而是(String)throws->()。如果一个函数接收另一个会throw的函数作为参数,那么其参数类型就需要进行相应的指定:


func receiveThrower(f:(String) throws -> ) {    // ...}  

现在,这个函数可以作为giveMeALongString的参数进行调用了:


func callReceiveThrower {    receiveThrower(giveMeALongString)}  

如果必要,匿名函数可以在正常的函数声明中使用关键字throws。不过,如果匿名函数的类型可以推断出来,那么这么做就没必要了:


func callReceiveThrower {    receiveThrower {        s in        if s.characters.count < 5 {            throw NotLongEnough.ISaidLongIMeantLong        }        print("thanks for the string")    }}  

Swift对throws函数的调用者也有要求:调用者必须要在调用前使用关键字try。该关键字告诉程序员和编译器,我们知道这个函数会throw。它还有这样一个要求:调用必须出现在throw为合法的情况下!使用try调用的函数会throw,因此try的含义就类似于throw:你必须要在do...catch结构的do块中或标记为throws的函数中使用它。

比如:


func stringTest {    do {        try giveMeALongString("is this long enough for you?")    } catch {        print("I guess it wasn't long enough: /(error)")     }}  

不过,如果你非常确定会throw的函数肯定不会throw,那么你就可以使用关键字try!而非try来调用它。这么做会简化使用:你可以在任何地方使用try!而无须捕获可能的throw。不过请注意:如果你做错了,当程序运行时这个函数抛出了异常,那么程序就会崩溃,因为你允许错误继续而没有捕获,一直到调用链的顶部。

因此,下面这种做法是合法的,但却是危险的:


func stringTest {    try! giveMeALongString("okay")}  

介于try与try!之间的是try?。它拥有try!的优点,你可以在任何地方使用它而无须捕获可能的异常。此外,如果真的抛出了异常,那么程序并不会崩溃;相反,它会返回nil。因此,try?在表达式返回一个值的情况下特别有用。如果没有抛出异常,那么它会将值包装到一个Optional中。一般来说,你可以通过条件绑定在同一行上安全地展开这个Optional。稍后将会介绍一个示例。

如果一个函数接收一个会抛出异常的函数作为参数,然后调用该函数(使用try),但结果没有抛出异常,那么我们可以将该函数本身标记为rethrows而非throws。二者的差别在于当调用一个rethrows函数时,调用者可以传递一个不抛出异常的函数作为参数。这样,就不必对调用使用try了(调用函数也无须标记为throws):


func receiveThrower(f:(String) throws -> ) rethrows {    try f("ok?")}func callReceiveThrower { // no throws needed    receiveThrower { // no try needed        s in        print("thanks for the string!")    }}  

下面来介绍一下Swift的错误处理机制与Cocoa和Objective-C之间的关系。常见的Cocoa模式是方法会通过返回nil来表示失败,并且接收一个NSError**参数作为与方法外部结果调用者之间通信的方式。Swift中该参数类型为NSErrorPointer,它是一个指向包装了NSError的Optional的指针。比如,NSString在Objective-C中有一个初始化器声明,如下所示:


- (instancetype)initWithContentsOfFile:(NSString *)path    encoding:(NSStringEncoding)enc    error:(NSError **)error;  

在Swift 2.0之前,该声明对应的Swift代码如下所示:


convenience init?(contentsOfFile path: String,    encoding enc: UInt,    error: NSErrorPointer)  

你可以将包装了NSError的一个Optional的地址作为最后一个参数传递给它:


var err : NSError?let s = String(contentsOfFile: f, encoding: NSUTF8StringEncoding, error: &err)  

调用完毕后,s要么是个String(包装在一个Optional中),要么是个nil。如果为nil,那么调用就失败了,你可以检查err,系统会设置其值来存储失败的原因。

不过在Swift 2.0中,该Objective-C方法会被自动进行类型转换,从而利用错误处理机制。error:参数已经从该声明的Swift版本中被移除了,并且被一个throws标记所替代:


init(contentsOfFile path: String, encoding enc: NSStringEncoding) throws  

因此,现在没必要提前声明好NSError变量了,也没必要间接地接收NSError。相反,你只需在Swift的控制条件中调用该方法即可:你需要在可能会抛出异常的地方使用try。结果永远不会为nil,因此也不会再有包装到Optional中的String了;它就是个String,因为如果初始化失败,那么调用会抛出异常,并不会产生任何结果:


do {    let f = // path to some file, maybe    let s = try String(contentsOfFile: f, encoding: NSUTF8StringEncoding)    // ... if successful, do something with s ...} catch {    print((error as NSError).localizedDescription)}  

如果非常确定初始化不会失败,那就可以省略do...catch结构,转而使用try!:


let f = // path to some file, maybelet s = try! String(contentsOfFile: f, encoding: NSUTF8StringEncoding)  

不过,如果有疑虑,那还可以省略do...catch结构并继续安全地使用try?,在这种情况下返回的值是个Optional,你可以安全地展开它,如以下代码所示:


let f = // path to some file, maybeif let s = try? String(contentsOfFile: f, encoding: NSUTF8StringEncoding) {    // ...}  

Objective-C NSError与Swift ErrorType是彼此桥接的。这样,在之前的catch块中,我将error变量类型转换为了NSError,并使用NSError属性检查它。不过,你不必这么做;相对于将捕获到的错误看作NSError,你可以将其看作Swift枚举。

对于常见的Cocoa错误类型,桥接枚举的名字就是NSError域的名字,同时将"Domain"从其名字中删除。假设文件不存在,调用会抛出异常,我们捕获到了错误。这个NSError的域就是"NSCocoaErrorDomain"。因此,Swift会将其看作一个NSCocoaError枚举。此外,其代码是260,这在Objective-C表示的是NSFileReadNoSuchFileError,在Swift则表示FileReadNoSuchFileError枚举。因此,我们可以像下面这样捕获这个错误:


do {    let f = // path to some file, maybe    let s = try String(contentsOfFile: f, encoding: NSUTF8StringEncoding)    // ... if successful, do something with s ...} catch NSCocoaError.FileReadNoSuchFileError {    print("no such file")} catch {    print(error)}   

参见Objective-C中的FoundationError.h头文件来了解关于Cocoa内建标准错误域的更多信息。

反之亦然。如前所述,采用了ErrorType的Swift类型会在背后自动实现其要求:特别地,其_domain是类型的名字,如果是枚举,那么其_code就是case的索引值(否则就是1)。如果在需要NSError的地方使用了ErrorType(或类型转换为NSError),那么它就会成为NSError的domain和code值。

3.Defer

Swift 2.0新增的defer语句的目的是确保某个代码块会在执行路径流经当前作用域时(无论如何流经)一定会执行。

Defer语句适用于它所出现的作用域,如函数体、while块、if结构等。无论在哪里使用defer,请在其外面使用花括号;当执行路径离开这些花括号时,defer块就会执行。离开花括号包括到达花括号的最后一行代码,或是本节之前介绍的任何一种形式的提前退出。

为了理解defer的作用,请看看如下两个命令:

UIApplication.sharedApplication().beginIgnoringInteractionEvents()

阻止所用用户触摸动作到达应用的任何视图。

UIApplication.sharedApplication().endIgnoringInteractionEvents()

恢复用户触摸到达应用视图的功能。

在一些耗时操作的开始停止用户交互,接下来当操作完毕时再恢复交互,特别是在操作过程中,用户轻拍按钮等会导致应用出错的场景下,使用defer是非常有用的。因此,有时我们会这样编写一些方法:


func doSomethingTimeConsuming {    UIApplication.sharedApplication.beginIgnoringInteractionEvents    // ... do stuff ...    UIApplication.sharedApplication.endIgnoringInteractionEvents}  

很不错,如果我们可以保证该函数的执行路径一定会到达最后一行。但如果需要从函数中提前返回呢?参见如下代码:


func doSomethingTimeConsuming {    UIApplication.sharedApplication.beginIgnoringInteractionEvents    // ... do stuff ...    if somethingHappened {        return    }    // ... do more stuff ...    UIApplication.sharedApplication.endIgnoringInteractionEvents}  

糟糕!我们犯了一个严重的错误。通过向doSomethingTimeConsuming函数提供一个额外的路径,代码可能永远都不会执行到对endIgnoringInteractionEvents()的调用。我们可以通过return语句离开函数,用户接下来就无法与界面交互了。显然,我们需要在if结构中添加另外一个endIgnoring...调用,就在return语句之前。不过,当继续编写代码时,要记住,如果添加离开函数的其他方式,那就需要对每一种方式都添加另外一个endIgnoring...调用,这简直太可怕了!

Defer语句可以解决这个问题。我们可以通过它指定当离开某个作用域时(无论如何离开)会发生什么事情。代码现在看起来如下所示:


func doSomethingTimeConsuming {    UIApplication.sharedApplication.beginIgnoringInteractionEvents    defer {        UIApplication.sharedApplication.endIgnoringInteractionEvents    }    // ... do stuff ...    if somethingHappened {        return    }    // ... do more stuff ...}  

Defer块中的endIgnoring...调用会执行,其执行时间并不取决于其出现的位置,而是在return语句之前或是在方法的最后一行代码之前,即执行路径离开函数的时刻。Defer语句表示:“最终(尽可能晚地),请执行该代码”。我们确保了关闭用户交互与打开用户交互之间的平衡。大多数defer语句的使用方式都是这样的:你通过它平衡一个命令或恢复受到干扰的状态。

如果当前作用域有多个defer块挂起,那么它们的调用顺序与其出现的顺序是相反的。实际上,有一个defer栈;每个后续的defer语句都会将其代码推至栈顶,离开defer语句的作用域会将代码弹出来并执行。

4.终止

终止是流程控制的一种极端形式;程序会在执行轨迹中突然停止。实际上,你可以故意让自己的程序崩溃。虽然,很少会这么做,但这却是给出危险信号的一种方式:你其实不想终止,这样一旦终止,那就表示一定出现了你无法解决的问题。

终止的一种方式是通过调用全局函数fatalError。它接收一个String参数,可以向控制台打印一条消息。如下示例之前已经介绍过了:


required init?(coder aDecoder: NSCoder) {    fatalError("init(coder:) has not been implemented")}  

上述代码表示,执行永远也不会到达这个点。我们并没有实现init(coder:),也不希望通过这种方式进行初始化。如果以这种方式初始化,那就说明有问题了,我们需要让程序崩溃,因为程序有严重的Bug。

包含了fatalError调用的初始化器不必初始化任何属性。这是因为fatalError是通过@noreturn特性声明的,它会让编译器放弃任何上下文的需求。与之类似,如果遇到了fatalError调用,那么拥有返回值的函数就不必返回任何值了。

还可以通过调用assert函数实现条件式终止。其第1个参数是个条件,值为一个Bool。如果条件为false,那就会终止;第2个参数是个String消息,如果终止,它会打印到控制台上。其功能是我们断言条件为true,如果条件为false,那么极有可能是程序中出现了Bug,你想让应用崩溃,这样就可以找出Bug并进行修复了。

在默认情况下,assert只在程序开发时会使用。当程序开发完毕并发布后,你会使用不同的构建开关,告诉编译器忽略assert。实际上,assert调用中的条件会被忽略;它们都会被当作true来看待。这意味着你可以放心地将assert调用放到代码中。当然,在交付程序时,断言是不应该失败的;会导致其失败的任何Bug都应该已经被解决了。

在交付代码时,对断言的禁用是通过一种很有意思的方式执行的。条件参数上会增加一个额外的间接层,这是通过将其声明为@autoclosure函数实现的。这意味着,虽然参数实际上不是函数,但编译器会将其包装为一个函数;这样一来,除非必要,否则运行时是不会调用该函数的。在交付代码时,运行时是不会调用该函数的。这种机制避免了代价高昂且不必要的求值:assert条件测试可能会有边际效应,不过如果程序中关闭了断言,那么测试就不会执行。

此外,Swift还提供了先决函数。它类似于断言,只不过在交付的程序中它依然是可用的。

5.Guard

如果需要跳转,那么你可能会测试一个条件来决定是否跳转。Swift 2.0为这种情况提供一个特殊的语法:guard语句。实际上,guard语句就是个if语句,你需要在条件失败时提前退出。其形式如示例5-6所示。

示例5-6:Swift guard语句


guard condition else {    statements    exit}  

如你所见,guard语句只包含一个条件和一个else块。else块必须要通过Swift所提供的任何一种方式跳出当前作用域,如return、break、continue、throw或fatalError等,只要确保编译器在条件失败时,执行不会在包含guard语句的块中继续即可。

这种架构的优雅结果在于,由于guard语句确保了在条件失败时退出,所以编译器就知道,如果没有退出,那么guard语句后的条件就是成功的了。这样,guard语句后条件中的条件绑定就处于作用域中,无须引入嵌套作用域。比如:


guard let s = optionalString else {return}// s is now a String (not an Optional)  

如前所述,该结构是“末日金字塔”的一个很好的替代方案。它与try?搭配起来使用也是非常方便的。假设只有在String(contentsOfFile:encoding:)成功时流程才能继续。接下来,我们可以重写之前的示例,如以下代码所示:


let f = // path to some file, maybeguard let s = try? String(contentsOfFile: f, encoding: NSUTF8StringEncoding)    else {return}// s is now a String (not an Optional)  

还有一个guard case结构,逻辑上与if case相反。为了说明,我们再次使用Error枚举:


guard case let .Number(n) = err else {return}// n is now the extracted number  

注意,guard语句的条件绑定不能使用等号左侧相同作用域中已经声明的名字。如下写法是非法的:


let s = // ... some Optionalguard let s = s else {return} // compile error  

原因在于,与if let和while let不同,guard let并不会为嵌套作用域声明绑定变量;它只会在当前作用域中声明。这样,我们就不能在这里声明s,因为s已经在相同作用域中声明了。