3. 流程控制

流程控制用來改變程式執行的順序。一般情況下,程式碼是從上到下一行一行依序執行,但我們可以透過迴圈語句讓程式碼重複不斷的執行,或是透過條件判斷語句讓程式碼在特定情況下才執行,代表有些程式碼可能不會執行到。

3.1 For-In迴圈

For-In迴圈的語法如下:

for 常數 in Sequence {
    // 要重複的程式碼寫這
}

Sequence是一個協定,符合這個協定型態有陣列、字典、字串或範圍運算子,Sequence中的元素個數用來決定For-In迴圈中程式碼的重複次數。例如,下述程式碼的陣列zoo中有四個元素,因此迴圈會執行四次,每一次會依序從陣列中從左至右取出一個元素放入常數animal中。陣列與字典語法請參考第4章聚集型態。

let zoo = ["獅子", "老虎", "大象", "長頸鹿"]
for animal in zoo {
    print("動物園有\(animal)")
}
// 動物園有獅子
// 動物園有老虎
// 動物園有大象
// 動物園有長頸鹿

也可以根據字典中的key來決定迴圈執行的次數,並且同時將value取出,例如:

let zoo = ["獅子": 5, "老虎": 3, "大象": 2, "長頸鹿": 6]
for (animal, number) in zoo {
    print("動物園有\(number)隻\(animal)")
}
// 動物園有5隻獅子
// 動物園有6隻長頸鹿
// 動物園有2隻大象
// 動物園有3隻老虎

也可以根據字串內容來決定重複次數,並且取得字串中的每一個字元。

for c in "hello" {
    print(c)
}
// h
// e
// l
// l
// o

若要根據數字範圍,可以使用範圍運算子。「n…m」代表由n到m包含m,或是「n..<m」代表由n到m-1,例如:

for i in 0...3 {
    print(i)
}
// 0
// 1
// 2
// 3
for i in 0..<3 {
    print(i)
}
// 0
// 1
// 2

~補充說明~
「…」與「..<」稱為範圍運算子,使用時「n…m」代表從n到m包含m,「n..<m」代表從n到m不包含m,「…m」代表從頭到m包含m,「..<m」代表從頭到m不包含m,「n…」代表從n開始一直到最後。除此之外,範圍運算子左側與右側的數字只能是整數型態,並且右側的值必須大於或等於左側的值。

For-In永遠的好朋友stride函數

由於範圍算子只能傳回由小到大排序的序列資料,如果希望資料內容為由大到小,或是每個元素的數字一次增加2,這時需要使用stride(from:to:by)這個函數了,例如:

for i in stride(from: 4, to: 1, by: -1) {
    print(i)
}
// 4
// 3
// 2

函數stride(from:to:by)會傳回一個sequence結構,如果將sequence轉成陣列,就可以清楚的看到這個sequence內容了,如下:

let array = Array(stride(from: 4, to: 1, by: -1))
print(array)
// Prints [4, 3, 2]

所以上述使用stride(from:to:by:)的迴圈,相當於下面這樣的程式碼:

for i in [4, 3, 2] {
    print(i)
}

函數stride(from:to:by:)的參數型態除了整數外,也可以是浮點數,例如:

let array = Array(stride(from: 2.0, to: 5.5, by: 0.5))
print(array)
// [2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0]

For-In迴圈中for後方的常數如果使用底線「_」,代表我們只在乎這個迴圈執行了幾次,並不在乎每一次從in後方序列中取出的元素內容,例如:

for _ in 1...3 {
    // 要重複3次的程式碼寫這
}

補充說明~
stride(from:to:by:)產生的值並不包含參數to,而stride(from:through:by:)產生的值可能會包含throuth參數,至於會不會包含要依據參數by所設定的步進值來決定。

巢狀迴圈

如果迴圈中還有迴圈稱為巢狀迴圈,例如以下程式碼產生九九乘法表。

for i in 2...9 {
    for j in 1...9 {
        print("\(i)x\(j)=\(i * j)", terminator:" ")
    }
    print()
}

/*
2x1=2 2x2=4 2x3=6 2x4=8 2x5=10 2x6=12 2x7=14 2x8=16 2x9=18
3x1=3 3x2=6 3x3=9 3x4=12 3x5=15 3x6=18 3x7=21 3x8=24 3x9=27
4x1=4 4x2=8 4x3=12 4x4=16 4x5=20 4x6=24 4x7=28 4x8=32 4x9=36
5x1=5 5x2=10 5x3=15 5x4=20 5x5=25 5x6=30 5x7=35 5x8=40 5x9=45
6x1=6 6x2=12 6x3=18 6x4=24 6x5=30 6x6=36 6x7=42 6x8=48 6x9=54
7x1=7 7x2=14 7x3=21 7x4=28 7x5=35 7x6=42 7x7=49 7x8=56 7x9=63
8x1=8 8x2=16 8x3=24 8x4=32 8x5=40 8x6=48 8x7=56 8x8=64 8x9=72
9x1=9 9x2=18 9x3=27 9x4=36 9x5=45 9x6=54 9x7=63 9x8=72 9x9=81
*/

3.2 While迴圈

While迴圈是透過依開始的判斷式來決定是否要重複執行while區段中的程式碼,語法如下。如果判斷式結果為true,就會進到while迴圈裡面去執行,每一遍執行完就會再檢查一次判斷式,直到判斷式結果為false才離開while迴圈。

while 判斷式 {
    // 要重複的程式碼寫這
}

下面這個例子,變數n初始化為0,所以一開始while迴圈的判斷式n < 5 會得到true,因此進到迴圈裡面執行。迴圈最後讓n的值加1,加完後會再檢查一次判斷式,因為此時n為1,所以判斷式運算結果還是true,於是繼續進到while迴圈裡面執行。不斷重複這樣的過程直到n等於5判斷式傳回false為止。

var n = 0
while n < 5 {
    print(n)
    n += 1
}
// 0
// 1
// 2
// 3
// 4

While迴圈通常使用於程式碼不知道需要重複幾次的場合,如果已經明確知道要重複幾次,通常會使用For-In迴圈,但並非絕對如此。以下面這個小遊戲為例,電腦會隨機出一個1到100之間的數字,然後使用者猜電腦出的是哪一個數字,直到猜對為止,因為不知道使用者會猜幾次才會猜對,所以這種情況使用while迴圈就很適合。

let answer = Int.random(in: 1...100)
var guess: Int!

print("1到100請猜一個數字: ", terminator: "")
guess = Int(readLine()!)
var times = 1
while (guess != answer) {
    if guess > answer {
        print("\(times)> 猜的太大了")
    } else {
        print("\(times)> 猜的太小了")
    }
    print("繼續猜: ", terminator: "")
    guess = Int(readLine()!)
    times += 1
}
print("恭喜答對了,總共猜了\(times)次")

~補充說明~
上述範例使用了標準輸入函數readLine(),因此需建立macOS的Command Line Tool專案執行,Playground專案無法執行此範例。

無窮迴圈

無窮迴圈代表這個迴圈不會結束,只要讓判斷式永遠為true就可以做出一個無迴圈,例如下述程式碼。無窮迴圈有時是在特定情況下必須寫成無窮迴圈,例如從網路讀取資料或是從攝影機取得影像資料,這時就需要透過無窮迴圈讓資料可以源源不絕的進來。

while true {
    // 要重複的程式碼寫這
}

無窮迴圈還是要有離開的時候,這時可以透過迴圈內部的控制指令來強制離開迴圈,本章稍後會說明。當然還有一些情況是程式寫錯了形成無窮迴圈。

3.3 Repeat迴圈

Repeat迴圈與while迴圈非常類似,只不過repeat迴圈將判斷式放在最後面,因此repeat迴圈會讓迴圈內的程式碼至少執行一次。語法結構如下:

repeat {
    // 要重複的程式碼寫這
} while 判斷式

以下述程式碼為例,計算出100以內的Fibonacci數列。

var (a, b) = (0, 1)
repeat {
    print(a, terminator:" ")
    (a, b) = (b, a + b)
} while a < 100
// 0 1 1 2 3 5 8 13 21 34 55 89

3.4 If…else…

If判斷式提供了在特定情形下才需要執行某些程式碼的能力,例如當氣溫超過28度時把窗戶關起來並且開冷氣否則就把窗戶打開並且把冷氣關掉。從這個敘述可以知道,「開冷氣關窗戶」與「關冷氣開窗戶」是互斥的兩段程式碼,他們不會同時執行,要執行哪一段程式碼是依據氣溫這個條件,而氣溫的判斷只會超過28度或沒超過28度這兩種狀況而已。如果寫成程式碼,如下:

if temperature > 28 {
    turnAirCondition(true)
    openWindows(false)
} else {
    turnAirCondition(false)
    openWindows(true)
}

有的時候if判斷式不一定需要else,例如判斷某個變數值如果是nil的話,就給一個非nil的值。

var n: Int?

if n == nil {
    n = 0
}

有的時候else後面還可以再接另外一個if語句,形成「假如…否則假如…否則…」這樣的語法結構。例如:

var aqi: Int = 80   // 空氣品質普通

if aqi <= 50 {
    print("空氣品質良好")
} else if aqi <= 100 {
    print("空氣品質普通")
} else if aqi <= 150 {
    print("空氣品質對敏感性族群不健康")
} else if aqi <= 200 {
    print("空氣品質不良")
} else if aqi <= 300 {
    print("空氣品質非常不良")
} else if aqi > 300 {
    print("空氣品質有害")
} else {
    print("未知")
}

這樣一連串的判斷式,只要上面的成立,即使之後還有判斷式也成立,也不會再進去執行。例如下面這段程式碼,雖然i < 5與i < 3 都成立,但是因為 i < 5 先成立,因此 i < 3 即使成立也不會執行。

var i = 2

if i < 5 {
    print("小於5")
} else if i < 3 {
    print("小於3")
}
// Prints "小於5"

巢狀判斷

當if判斷式中還包含了其他if判斷式即形成巢狀判斷,例如下面這段程式碼,只有當 flag為true以及n > 0的時候才會將n重新設定為0。

var flag = true
var n = 20

if flag {
    if n > 0 {
        n = 0
    }
}

比較運算子

兩個變數或常數需靠算數運算子來進行運算,例如常見的加、減、乘、除運算子。在if判斷式中,須靠比較運算子才能產生邏輯結果也就是布林值(true或false),因此我們需要知道哪些運算子可以產生布林值,這樣的運算也稱為布林運算。

比較運算子範例說明
==a == 10a的內容等於10傳回true
===obj1 === obj2obj1與obj2指向同一個物件實體傳回true
a > 10a的內容大於10傳回true
a < 10a的內容小於10傳回true
>=a >= 10a的內容大於等於10傳回true
<=a <= 10a的內容小於等於10傳回true
!=a != 10a的內容不等於10傳回true

~補充說明~
除了比較運算子之外,「is」用來判斷是否屬於某個資料型態,雖然判斷結果也是布林值,例如 if a is String { … },但在定義上is不屬於比較運算子。此外,is 的右側一定要放資料型態,不可以放變數或常數來比較is兩側是否同屬於一個型態。

邏輯運算子

邏輯運算子用來連結兩個邏輯運算式,有三種:邏輯且(&&)、邏輯或(||)與邏輯否(!)。例如下面判斷式的運算結果會成立(結果為true),因為a > 0成立而且b < 30 也成立。

var a = 5, b = 10
if a > 0 && b < 30 {
    
}

以下判斷式也會成立,因為a > 0或b < 0只要其中一個成立整個判斷式就成立。

var a = 5, b = 10
if a > 0 || b < 0 {
    
}

邏輯運算子的真值表如下所示,A與B分別代表兩個布林運算式結果:

邏輯且(&&)

ABA && B
truetruetrue
truefalsefalse
falsetruefalse
falsefalsefalse

邏輯或(||)

ABA || B
truetruetrue
truefalsetrue
falsetruetrue
falsefalsefalse

邏輯否(!)

A!A
truefalse
Falsetrue

邏輯且(&&)在if判斷式中也可以使用逗點「,」取代,例如:

var a = 5, b = 10
if a > 0, b < 30 {

}

3.5 判斷nil

當運算式中如果某變數或常數的內容為nil,接下來如果透過該變數或常數進行運算就會讓程式當掉,因此運算前檢查數值內容是否為nil是一件必須要做的工作。例如下面這段程式碼,當字串要轉型為整數時有的時候無法轉換,例如當字串內容為”abc”這種非數字的時候,轉換結果必定為nil。若此時沒有判斷變數a的值是否為nil就要進行之後的加法運算,程式必定當掉。

var str: String = "10"
var a = Int(str)
if a != nil {
    a! += 1
    print(a!)
}
// Prints "11"

再看另外一個例子。若變數a的值必須要為正整數時才要進行運算,因此透過判斷式檢查a是否為正整數,但a的型態為Optional型態,表示a的內容有可能出現nil,而nil是不可以跟0來比較的,一比較就會讓程式當掉,所以在判斷a是否為正整數之前必須先判斷a 是否為nil,如果不是才能比較a是否大於0。

var a: Int?
if a != nil {
    if a! > 0 {
        a! += 1
    }
}

我們可以將這兩個if語句合併成一個,合併後結果如下:

var a: Int?
if a != nil,  a! > 0 {
    a! += 1
}

Swift的判斷式如果由兩個以上的判斷式組合而成,並且只要已經能夠判定出最後結果,這時就不會將全部的判斷式執行完畢。例如當a為nil時,並不用擔心後面的a! > 0 會導致程式當掉,因為當a為nil時,第一個判斷式為false,因此不論後面的布林運算結果為何,整個結果一定是false,此時 a! > 0 不會運算。但是如果將這兩個運算式順序對調就不行了,例如下面的程式碼。當a為nil時第一個判斷式的運算結果必定讓程式當掉。因此,在任何需要判斷是否為nil的時候這個判斷式都應該放在最前面。

if a! > 0, a != nil {
    a = a! + 1
}
// crash

~補充說明~
並非每一種程式語言都有這樣的特性,如果不確定的時候,建議還是使用巢狀判斷比較妥當。

if let 與 if var

在說明if let與if var語句前,我們先來看以下的例子。在這個例子中,如果str不是nil就會進到if裡面去執行相關的程式碼。由於在if語句中已經確定此時str不是nil,因此使用str!把值取出時不會導致程式當掉。

var str: String?
if str != nil {
    print(str!)
}

上述程式中在if區段內的程式碼,取用str內容時都必須使用str!,但因為此時已經確定str不可能為nil,所以我們希望能夠省略「!」符號,讓程式碼簡潔一些。這時我們可以把程式改成如下的程式碼,只要使用常數newstr就可以不用符號「!」。

if str != nil {
    let newstr = str!
    print(newstr)
}

Swift的if let語句把上述程式碼中的if與let合併成一行,形成如下的程式碼:

if let newstr = str {
    print(newstr)
}

Swift也允許if let後面的名稱與原本的名稱一致,於是形成以下經常看到的程式碼。需要注意的是,等號左側的str與右側的str不是同一個,只是名字剛好一樣而已,左側的str有效範圍僅止於if判斷式成立時。

if let str = str {
    print(str)
}

如果想要在判斷式成立時修改左側str的內容,只要將if let改為if var即可,如下:

if var str = str {
    str = str + "\n\n" + str
    print(str)
}

if let或if var可與其他邏輯運算式一起使用,下面程式碼是在變數a不是nil且a > 0的情況下,計算a * 2。

var a: Int? = 10
if let tmp = a,  tmp > 0 {
    a = tmp * 2
}

3.6 Switch…Case

Switch語句適合用來判斷數值中的各種不同狀態,雖然使用if語句也可以分辨各種狀態,但switch語句能夠做的事情更多。Switch語句由switch與case組合而成,依據switch後面變數、常數或運算的結果,撰寫不同的case來處理各種不同情況下要做的事情,例如下面程式碼中score >= 60的結果只有兩種狀態,成績不是及格true就是不及格false,因此就用兩個case來處理true與false下要做的事情。

var score = 80

switch score >= 60 {
case true:
    print("成績及格")
case false:
    print("成績不及格")
}

有的時候我們必須要在switch中加上default區段,這個區段代表目前列出的case沒有把全部的狀態都列完,因此沒有處理完的部分就由default去處理。以下面這段程式碼為例,字串light可以表示出交通號誌的綠燈、黃燈與紅燈,所以使用三個case來表示這三種燈號。但因為字串內容太多了,此時無法判定是否只有這三種字串,因此編譯器會強制要求在switch中加上default區段。

var light: String? = "黃"

switch light {
case "紅":
    print("禁止通行")
case "黃":
    print("準備停止")
case "綠":
    print("可以通行")
default:
    print("燈號故障")
}

此外,數字型態是另一種在switch語句中常出現的型態,以空氣品質指標AQI為例,由於AQI會依據值的範圍來判定目前的空氣品質狀態並給出建議,因此在case中使用範圍運算子「…」來標定一個範圍,如下:

var aqi: Int = 80

switch aqi {
case 0...50:
    print("空氣品質良好")
case 51...100:
    print("空氣品質普通")
case 101...150:
    print("空氣品質對敏感性族群不健康")
case 151...200:
    print("空氣品質不良")
case 201...300:
    print("空氣品質非常不良")
case 301...:
    print("空氣品質有害")
default:
    print("未知")
}

~補充說明~
「301…」相當於「301…Int.max」,Int.max省略了。

在case語句上也可以將多個case合併成一個,每個項目間用逗點隔開,相當於「或」的意思。

var star = "地球"

switch star {
case "太陽":
    print("恆星")
case "地球", "水星", "金星", "木星":
    print("行星")
default:
    print("其他星球")
}

Switch後方也可以使用於tuple格式的資料,例如下述程式碼。

let resolution = (1024, 768)
var des: String

switch resolution {
case (320, 240):
    des = "QVGA"
case (640, 480):
    des = "VGA"
case (1024, 768):
    des = "XGA"
case (_, _):
    des = "其他"
}
print(des)
// Prints "XGA"

「case (_, _)」代表不論tuple中的數字為何,都會進入這個case,有點類似default,但default必須放在所有case之後而case (_, _)可以放在任何地方。當然如果將case (_, _)放在第一個case的位置,不論resolution的值為何,永遠只會進入這個case了。「_」相當於「don’t care」,不在乎這個值為何的意思。

數值綁定

Switch可以將對應到case的數值綁定到一個常數上,這在資料分類上非常好用,舉例如下:

var student: (String, String)
student = ("王小毛", "甲班")

switch student {
case (let name, "甲班"):
    print("\(name)在甲班")
case (let name, "乙班"):
    print("\(name)在乙班")
case (let name, let `class`):
    print("\(name)在\(`class`)班")
}
// Prints "王小毛在甲班"

~補充說明~
由於class是保留字用於類別宣告,因此變數或常數名稱要取名為class時,必須使用backtick符號「`class`」。

Where語句

在case中使用where語句可以讓綁定的常數加上額外的邏輯判斷,例如我們只想找出甲班姓王與姓李的學生,可以寫成如下的程式碼:

var student: (String, String)
student = ("王小毛", "甲班")

switch student {
case (let name, "甲班") where name.hasPrefix("王"):
    print("甲班中姓王的學生有\(name)")
case (let name, "甲班") where name.hasPrefix("李"):
    print("甲班中姓李的學生有\(name)")
case (_, _):
    break
}
// Prints "甲班中姓王的學生有王小毛"

~補充說明~

  1. 如果case區段中沒有任何程式碼時,必須加上break語句。
  2. where語句中也可以放入兩個以上的邏輯運算式。

3.7 Break、continue與fallthrough

在迴圈中可以使用break與continue改變迴圈的行為,For-In、While與Repeat都可以使用。

Break

在迴圈中如果遇到break會立刻離開迴圈,例如下面程式碼中迴圈執行到「大象」時會立刻結束。

let zoo = ["獅子", "老虎", "大象", "長頸鹿"]
for animal in zoo {
    if animal == "大象" {
        break
    }
    print("動物園有\(animal)")
}

// 動物園有獅子
// 動物園有老虎

break也常用來在無窮迴圈中遇到特定狀況時離開迴圈。下面這段程式碼是模擬倒數計時,倒數到0時結束迴圈。sleep(1)為Swift標準函數庫中的函數,目的是讓程式運行暫停1秒鐘。

var n = 10
while true {
    print(n)
    sleep(1)
    n -= 1
    if n < 0 {
        break
    }
}

Continue

Continue的作用跟break不一樣的地方在於,continue會立刻停止這一次的執行並且回到迴圈的判斷處(For-In迴圈會回到開頭處)來決定繼續執行迴圈還是要結束迴圈。以動物園為例,迴圈執行到大象時,該次後面的程式碼就不執行了,但是迴圈不會結束,因為還有長頸鹿這筆資料需要執行。如下:

let zoo = ["獅子", "老虎", "大象", "長頸鹿"]
for animal in zoo {
    if animal == "大象" {
        continue
    }
    print("動物園有\(animal)")
}
// 動物園有獅子
// 動物園有老虎
// 動物園有長頸鹿

Fallthrough

fallthrough專門用在switch語句,由於case項目執行完之後並不會繼續執行下一個case中的內容,因此在switch中,每一個case可以省略break語句。但如果我們希望可以繼續執行下一個case或default中的程式碼時,加上fallthrough保留字就可以。例如我們買了一個環保杯,我們想要知道這個環保杯可以在咖啡店裝何種容量的咖啡時,程式碼如下:

let ml = 500
switch ml {
case 591...:
    print("特大杯")
    fallthrough
case 473...:
    print("大杯")
    fallthrough
case 354...:
    print("中杯")
    fallthrough
case 236...:
    print("小杯")
default:
    print("這杯子有問題")
}
// 大杯
// 中杯
// 小杯

所以當我們買了一個500ml的杯子,我就可以知道他可以裝大杯、中杯、小杯,而特大杯是裝不下的。但特別注意fallthrough並不是讓每一個case都判斷一次,而是只要有case符合了,下面其他的case包含default中的程式碼「必定執行」。

3.8 Guard

Guard語句用來檢測運算結果是否為true,如果是true代表檢測通過,如果為false,則會執行else區段中的程式碼。語法如下:

guard condition else {
    // condition 為 false 要做的事情寫這
}

Guard語句通常在函數中使用,目的是確保傳進去的參數值必須符合某些條件,如果不符合條件則在else中透過return語句讓函數提前返回。例如下面這個除法運算的函數,當傳進去的分母為零的時候,必須提前離開這個函數,否則執行到最後的除法運算時程式必定當掉。

func divided(_ a: Double, by b: Double) -> Double? {
    guard b != 0 else {
        print("分母不可為零")
        return nil
    }
    return a / b
}

Guard語句與if語句功能很類似,但guard比if單純許多。Guard沒有巢狀判斷,也只有唯一的else區段,當判斷式結果為true的時候就會離開guard。所以guard非常適合用來檢測參數值是否符合特定要求,如果能夠通過guard檢測,代表這些參數值都沒有問題了。

如果傳進來的參數為Optional型態而且需要排除nil時,guard可以跟let一起合併使用,例如下面的程式碼透過三個guard語句來保證執行到除法運算時,分子分母都不會是nil,而且分母不會是零。

func divided(_ a: Double?, by b: Double?) -> Double? {
    guard let a = a else {
        print("參數不可為nil")
        return nil
    }
    guard let b = b else {
        print("參數不可為nil")
        return nil
    }
    guard b != 0 else {
        print("分母不可為0")
        return nil
    }
    return a / b
}

也可以將上述的三個guard合併成一個,程式碼如下。跟if語句一樣,每個判斷式中間的逗號相當於邏輯且&&,意思是三個判斷式都必須為true,合起來的結果才會是true。所以這個guard語句就可以保證,執行到除法運算時分子與分母的值都符合除法運算要求。

func divide(_ a: Double?, by b: Double?) -> Double? {
    guard let a = a, let b = b, b != 0 else {
        print("輸入值錯誤")
        return nil
    }
    return a / b
}

3.9 Defer

Defer的作用是延遲執行。使用defer語句包起來的程式碼,會在defer所在的區段結束前執行。例如下面這段程式碼,defer中的print()會在函數結束前才呼叫,因此”hello, world”會先印出,最後才印出defer中的字串。

func f() {
    defer {
        print("function will close")
    }
    print("hello, world")
}

f()
// Prints "hello, world"
// Prints "function will close"

即使函數中丟出錯誤,defer也會執行。throw請參考第16章錯誤處理。但如果是斷言(請見下一節)造成的程式結束,defer就不會執行。

enum MyError: Error {
    case err
}

func f() throws {
    defer {
        print("function will close")
    }
    print("hello, world")
    throw  MyError.err
}

do {
    try f()
} catch {
    print(error)
}

Defer很常見於跟檔案存取有關的函數。由於檔案開啟後,最後一定要關檔,但中間可能發生很多情形導致提前離開函數,所以需要在這些會提前離開函數的很多地方都要執行關檔指令,只要有一個地方漏掉就會造成還沒關檔就離開函數的錯誤。為了避免未關檔的情形發生,可以在開檔後立刻將關檔程式放在defer中,這樣不論在何時函數結束都一定會關檔。

func write(_ data: Data) {
    guard let f = FileHandle(forUpdatingAtPath: "a.txt") else {
        print("開檔失敗")
        return
    }
    
    defer {
        try? f.close()
    }
    
    // 檔案存取程式碼寫這
    f.write(data)
}

如果有多個defer,defer中的程式碼會放在堆疊中,因此每個defer會以後進先出的方式依序執行,例如:

func f() {
    defer { print(1) }
    defer { print(2) }
    defer { print(3) }
    print("hello, world")
}

f()
// Prints "hello, world"
// Prints "3"
// Prints "2"
// Prints "1"

3.10 斷言與先決條件

我們在程式運行發生錯誤時,常常需要回頭去找到底是哪一行程式碼造成的錯誤。為了確認是哪一行程式碼有問題,我們會在許多位置設中斷點暫時停止程式執行,然後將變數或常數值印出來看看,確認當時的資料內容是否跟預期相同。然後不斷重複這樣的工作,直到錯誤修正為止。但有些錯誤即使透過中斷點或單步追蹤等這些除錯功能還是找不到問題所在,這時可能就要靠斷言機制(assertion)來幫忙了。

斷言跟設中斷點很類似,都是在特定的地方去檢查相關的資料是否滿足預期狀態,如果符合預期程式就繼續執行下去,如果跟預期的不合,就立刻停掉程式,然後印出客製化的錯誤訊息以及斷言所在的檔案名稱與行號。但跟中斷點不同的地方在於,設定了中斷點程式必定在那個位置停下來,但斷言只有在需要停的時候才會停下來,如果執行狀態一切都好好的,就會一直執行下去。

我們看下面這個例子。如果變數value的值為負的,代表程式有問題,應該是某個地方讓value的值變成負的,為了找出這個錯誤,所以在所有修改value值的地方加上斷言函數assert(_:)來找出到底問題在哪裡。assert(_:)第一個參數為判斷式,在這裡填入value > 0,所以只要value為正就表示通過檢測,程式繼續執行下去,如果為負,代表就是這個地方出現問題,程式立刻中止執行,並印出相關的錯誤訊息。

var value = -3
assert(value > 0, "value = \(value)")
// Assertion failed: value = -3: file MyPlayground.playground, line 20

除了兩個參數的assert函數外,另外還有一個參數的assert。如果當錯誤發生且不需要顯示特別客製化的訊息時,可以使用一個參數的assert。錯誤發生時還是會印出訊息,這些訊息包含了檔名與行號,只是沒有客製化的訊息而已。

var value = -3
assert(value > 0)
// Assertion failed: file MyPlayground.playground, line 20

若需要不管任何原因程式都要中止程式執行的情況時,可以使用assertionFailure()與assertionFailure(_:) 這兩個函數。前者不帶客製化錯誤訊息,後者可以加上客製化錯誤訊息。什麼時候會需要用到任何情況都要中止執行呢?舉個例子,可以在switch語句中的default狀態時加上這兩個函數,因為我們已經確定所有狀況都已經透過各個case處理了,在正常情況下程式不應該會執行到default區段,所以在default區段中加上這兩個函數中的一個來看看如果真的程式執行到default時是什麼狀況。

這四個斷言函數跟中斷點一樣,只有在DEBUG模式中才會發生作用,在RELEASE模式時斷言函數會自動失效,因此也跟中斷點一樣,不需要在App送審或是正式發佈時將所有斷言函數刪除。

與斷言相同功能的有另外四個函數稱為先決條件,用法跟斷言的四個函數完全一樣,先決條件的函數名稱分別為:precondition(_:_:)、precondition(_:)、preconditionFailure()、preconditionFailure(_:)。先決條件與斷言的差異在於先決條件可同時作用於DEBUG與RELEASE模式。

發表迴響