讀古今文學網 > iOS編程基礎:Swift、Xcode和Cocoa入門指南 > 5.1 流程控制 >

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 cases
if 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)?.doubleValue
if 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:
    statements
case pattern2:
    statements
default:
    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: break
default:
    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.beQuiet
case 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: break
case .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": fallthrough
case "Moe": fallthrough
case "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 error
case .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 = textField
repeat { v = v.superview! } while !(v is UITableViewCell)
  

上述代碼執行完畢後,v就是我們要找的表視圖單元。不過,上述代碼有些危險:如果在到達視圖層次結構的最頂層時沒有遇到UITableViewCell(也就是說superview為nil的視圖),那麼程序就會崩潰。下面以一種安全的方式來編寫同樣的代碼:


var v : UIView = textField
while 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 = 0
while 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).generate
while let i = g.next {
    print(i)
}
  

有時,你會發現像上面這樣顯式使用while循環會使得循環更易於控制和定制。

序列通常是個已經存在的值。它可以是字符序列,這樣變量值就是連續的Character。它可以是Array,這樣變量值就是連續的數組元素。它可以是字典,這樣變量值就是鍵值對元組,你可以將變量表示為兩個名字的元組,從而可以捕獲到它們。之前的章節中已經介紹了一些示例。

正如第4章所述,你可能會遇到來自於Objective-C的數組,其元素需要從AnyObject進行向下類型轉換。我們常常會將其作為序列規範的一部分:


let p = Pep
for 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 : Int
for i = 1; i < 6; i++ {
    print(i)
}
  

要想將計數器的作用域限制到花括號中,請在before-all語句中聲明它:


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

不過,沒有規則說這種形式的for循環就一定是與計數或值增加相關的。回憶一下之前介紹的關於while循環的示例,我們遍歷了視圖層次,查找某個表視圖單元:


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

下面是另一種做法,使用一個for循環,其代碼塊是空的:


var v : UIView
for 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, maybe
let s = try! String(contentsOfFile: f, encoding: NSUTF8StringEncoding)
  

不過,如果有疑慮,那還可以省略do...catch結構並繼續安全地使用try?,在這種情況下返回的值是個Optional,你可以安全地展開它,如以下代碼所示:


let f = // path to some file, maybe
if 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, maybe
guard 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 Optional
guard let s = s else {return} // compile error
  

原因在於,與if let和while let不同,guard let並不會為嵌套作用域聲明綁定變量;它只會在當前作用域中聲明。這樣,我們就不能在這裡聲明s,因為s已經在相同作用域中聲明了。