Swift學習(2)- 基礎部分(程式碼完善版)

Swift學習(2)- 基礎部分(程式碼完善版)

August 7, 2021·Jingyao Zhang
Jingyao Zhang

image

Swift包含了CObjective-C所有的基本資料型別,Int代表整數值;DoubleFloat代表浮點數值;Bool是布林型別;String則是文字型資料。Swift還提供了三種基本的集合型別,分別是ArraySetDictionary

就像C語言一樣,Swift使用變數來儲存資料並透過變數名稱來關聯值。在Swift中,廣泛地鼓勵使用不可變的變數,也就是常數,這比C語言中的常數更強大。在Swift中,如果某個值不需要變動,使用常數可以讓程式碼更安全,也能更清楚地表達意圖。

除了我們熟悉的型別,Swift還新增了Objective-C所沒有的高階資料型別,例如元組(Tuple)。元組可以讓使用者建立或傳遞一組資料,例如作為函式的回傳值時,可以用一個元組回傳多個值。

Swift還新增了一個可選型別(Optional),用來處理值可能不存在的情況。可選型別的意思是「這裡有一個值,且它等於x」或「這裡沒有值」。可選型別有點像在Objective-C中使用nil,但它可以用在任何型別上,不僅僅是類別。可選型別比Objective-C中的nil指標更安全且更具表現力,也是Swift許多強大功能的重要基礎。

Swift是一個型別安全的語言,這代表Swift可以讓使用者明確知道值的型別。如果程式需要一個String參數,型別安全會阻止你不小心傳入一個Int參數。同樣地,如果程式需要的是String參數,型別安全也會阻止你意外傳入一個可選的String。型別安全可以幫助開發者在開發階段及早發現並修正錯誤。


常數與變數

常數與變數將一個名稱(如testVal)與一個指定型別的值(如「Jensen」)關聯起來。常數的值一旦設定就不能再改變,而變數的值可以隨時更改。

宣告常數與變數

常數與變數必須在使用前宣告,使用let來宣告常數,使用var來宣告變數。

let maximumNumberOfLoginAttempts = 10  // 常數,記錄可登入嘗試的最大次數
var currentLoginAttempt = 0  // 變數,記錄目前登入嘗試次數

上述兩行程式碼可以理解為:「宣告一個名稱為maximumNumberOfLoginAttempts的新常數,並給它一個值10。然後,宣告一個名稱為currentLoginAttempt的變數並將它的值初始化為0。」

你可以在同一行宣告多個常數或多個變數,使用逗號分隔。

var x = 0.0, y = 0.0, z = 0.0

注意

如果程式中有不需要變動的值,請使用let關鍵字將其宣告為常數。只有需要變動的值才宣告為變數。

型別註解

宣告常數或變數時可以加上型別註解(Type Annotation),說明常數或變數要儲存的值的型別。若要加上型別註解,需要在常數或變數名稱後面加上一個冒號和空格,然後加上型別名稱。

var welcomeMessage: String // 給welcomeMessage變數加上型別註解,表示這個變數可以儲存String型別的值
// 上述宣告中的冒號代表「是...型別」,welcomeMessage變數現在可以被設定為任何字串。
welcomeMessage = "Hello!"

甚至可以在同一行定義多個相同型別的變數,用逗號分隔,並在最後一個變數名稱後加上型別註解。

var red, green, yellow: Double

常數與變數的命名

常數與變數名稱幾乎可以包含所有字元,包括Unicode字元。

let π = 3.141592654
let 你好 = "你好世界!"
let 🐮🍺 = "666"

但也有例外,常數與變數名稱不能包含數學符號、箭頭、保留(或非法)的Unicode碼位、連線與定位字元。也不能以數字開頭,但可以在名稱的其他地方包含數字。

一旦給常數或變數加上型別註解,就不能用相同名稱再次宣告,或改變其儲存值的型別。同時,Swift也不允許變數與變數之間互轉型別。

注意

如果需要使用與Swift保留關鍵字相同的名稱作為常數或變數名稱,可以用反引號(`)將關鍵字包起來作為名稱。不過,開發者應避免使用保留關鍵字作為名稱,除非真的別無選擇。

可以更改現有變數的值為其他相同型別的值。

var friendlyWelcome = "Hello!"
friendlyWelcome = "Benjour!"  // friendlyWelcome現在是「Bonjour!」
friendlyWelcome = "你好!" // friendlyWelcome現在是「歡迎!」

與變數不同,常數的值一旦確定就不能更改。嘗試這麼做會在編譯時出錯。

let languageName = "Swift"
//languageName = "C++"  // 取消這行註解編譯器會報錯:Cannot assign to value: 'languageName' is a 'let' constant

輸出常數與變數

print(friendlyWelcome)  // 輸出:你好!
---
output: 你好!

separatorterminator參數有預設值,因此呼叫這個函式時可以忽略它們。

預設情況下,輸出函式會在結尾自動加上換行符。如果不想換行,可以傳遞空字串給terminator參數。

print("Line1", terminator: "")
print("Line2")
---
output: Line1Line2

Swift用字串插值(String interpolation)的方式,將常數或變數名稱作為佔位符插入長字串中。Swift會用目前常數或變數的值取代這些佔位符。將常數或變數名稱放入小括號中,並在前面加上反斜線。

print("The current login attempt is \(currentLoginAttempt), you have \(maximumNumberOfLoginAttempts) login attempts in total.")
---
output: The current login attempt is 0, you have 10 login attempts in total.

註解

Swift中的註解與C語言的註解非常類似,單行註解以兩個斜線//作為起始。

也可以寫多行註解,起始標記為/*,結束標記為*/

/* 這是一個註解
 但這是多行註解 */

C語言不同的是,Swift的多行註解可以巢狀在其他多行註解之中。

/* 這是外層註解的第一行
 /* 這是內層註解 */
 這是外層註解的最後一行 */

利用巢狀多行註解,可以快速方便地註解掉一大段程式碼,即使這段程式碼中已經有多行註解區塊。


分號

與大多數其他程式語言不同,Swift不強制要求開發者在每條語句結尾加上分號;。當然,開發者也可以依照習慣加分號,但有一種情況必須加分號,就是在同一行寫多條獨立語句時。

let pig = "🐷"; print(pig)
---
output: 🐷

整數

整數就是沒有小數部分的數字,例如43和-23。Swift提供8、16、32和64位元的有號與無號整數型別。這些型別的命名方式與C語言類似,例如8位元無號整數型別是UInt8,32位元有號整數型別是Int32。就像Swift的其他型別一樣,整數型別採用大寫命名。

整數範圍

可以透過不同整數型別的minmax屬性來取得對應型別的最小值與最大值。

let minValue = UInt8.min
let maxValue = UInt8.max
print("UInt8型別的最小值是:\(minValue)\nUInt8型別的最大值是:\(maxValue)")
---
output: UInt8型別的最小值是0
UInt8型別的最大值是255

minmax回傳值的型別就是對應的整數型別,如上例UInt8,回傳值型別也是UInt8

Int

一般來說,不需要特別指定整數的長度。Swift提供一個特殊的整數型別Int,長度與目前平台的原生字長相同:

在32位元平台上,Int與Int32長度相同

在64位元平台上,Int與Int64長度相同

UInt

Int型別一樣,Swift也提供一個特殊的無號型別UInt,長度與目前平台的原生字長相同:

在32位元平台上,UInt與UInt32長度相同

在64位元平台上,UInt與UInt64長度相同

注意

除非真的需要儲存與平台原生字長相同的無號整數,否則盡量不要使用UInt。建議統一使用Int,這樣可以提升程式碼的可重用性,避免不同型別數字之間的轉換,也方便型別推斷。


浮點數

浮點數就是有小數部分的數字,例如3.141592654、0.1和-98.09等。

浮點型別比整數型別能表示更大的範圍,可以儲存比Int型別更大或更小的數字。Swift提供兩種有號浮點數型別:

Double代表64位元浮點數,當需要儲存很大或高精度的浮點數時請用此型別

Float代表32位元浮點數,若精度要求不高可用此型別

注意

Double的精確度較高,至少有15位小數,而Float只有6位小數。選擇哪種型別取決於你的程式需要處理的數值範圍,在兩者都適用時會優先選擇Double。


型別安全與型別推斷

Swift是一個型別安全(Type Safe)的語言。型別安全的語言可以讓開發者明確知道程式要處理的值的型別。

由於Swift是型別安全的,所以它在編譯程式時會進行型別檢查(Type Checks),並將不相容的型別標記為錯誤。這能讓開發者及早發現錯誤。

當要處理不同型別的值時,型別檢查可以幫助避免錯誤。但這並不代表每次宣告常數與變數時都要明確指定型別。如果沒有明確指定型別,Swift會使用型別推斷(Type Inference)來選擇合適的型別。原理很簡單,只要檢查賦值即可。

當宣告常數或變數並給初值時,型別推斷非常有用。只要在宣告時給它們一個字面值(Literal Value),就會觸發型別推斷。(字面值就是在程式中直接出現的值,例如0.1和3.141592654等)

let meaningOfLife = 42  // meaningOfLife會被型別推斷為Int型別
let pi = 3.141592654  // pi會被型別推斷為Double型別,推斷浮點數型別時,Swift總是選擇Double而非Float

如果運算式中同時出現整數與浮點數,會被推斷為Double型別。

let anotherPi = 3 + 0.141592654  // 原始值3沒有明確宣告型別,後面又有浮點字面值,所以運算式被推斷為Double型別

數值型字面值

整數字面值可以寫成:

十進位數字,無前綴

二進位數字,前綴為0b;八進位數字,前綴為0o;十六進位數字,前綴為0x

以下所有整數字面值的十進位值都是17:

let decimalInteger = 17
let binaryInteger = 0b10001  // 二進位的17
let octalInteger = 0o21  // 八進位的17
let hexadecimalInteger = 0x11  // 十六進位的17

小數點兩邊必須至少有一個十進位數字或十六進位數字。十進位浮點數也可以有可選的指數(Exponent),用大寫或小寫的e指定;十六進位浮點數必須有指數,用大寫或小寫的p指定。

十進位:1.25e2代表1.2510^2,等於125.0;八進位:0xFp-2代表152^-2,等於3.75

以下這些浮點字面值都等於十進位的12.1875。

let decimalDouble = 12.1875
let exponentDouble = 1.21875e1
let hexadecimalDouble = 0xC.3p0

數值型字面值可以包含額外的格式來增進可讀性。整數與浮點數都可以加上額外的零並包含底線,這不會影響字面值的數值。

let paddedDouble = 000123.456
let oneMillion = 1_000_000
let justOverOneMillion = 1_000_000.000_000_1

數值型型別轉換

通常來說,即使程式中的整數常數與變數已知為非負,也請使用Int型別。只有在必要時才使用其他整數型別,例如要處理外部長度明確的資料或為了最佳化效能、記憶體等。使用明確指定長度的型別可以及早發現值溢位,也能暗示正在處理特殊資料。

整數轉換

不同整數型別的變數與常數可以儲存不同範圍的數字。Int8型別的常數或變數可儲存的數字範圍為-128~127;而UInt8型別的常數或變數可儲存的數字範圍是0~255。如果數字超出可儲存範圍,編譯時會報錯。

// let cannotBeNegative: UInt8 = -1  // 取消這行註解會報錯:Negative integer '-1' overflows when stored into unsigned type 'UInt8'
// let tooBig: Int8 = Int8.max + 1  // 取消這行註解會報錯:Arithmetic operation '127 + 1' (on type 'Int8') results in an overflow

開發者需要根據情況選擇是否進行數值型型別轉換,這種選擇性使用方式可以預防隱式轉換的錯誤,並讓程式中的型別轉換意圖更明確。

let twoThousand: UInt16 = 2_000
let one: UInt8 = 1
let twoThousandAndOne = twoThousand + UInt16(one)  // 這裡用UInt16(one)建立一個新的UInt16數字並用one的值初始化,然後就能讓兩個無號16位元值相加
print("UInt8轉換至UInt16: ", twoThousandAndOne)
---
output: UInt8轉換至UInt16:  2001

SomeType(ofInitialValue)是呼叫Swift建構子並傳入初始值的預設方法。在語言內部,UInt16有一個建構子,可以接受一個UInt8型別的值,所以這個建構子可以用現有的UInt8來建立一個新的UInt16

整數與浮點數轉換

整數與浮點數的轉換必須明確指定型別。

let three = 3
let pointOneFourOneFiveNine = 0.14159
let simplePi = Double(three) + pointOneFourOneFiveNine  // pi等於3.14159,所以被型別推斷為Double型別
print("整數與浮點數轉換:", simplePi)
---
output: 整數與浮點數轉換: 3.14159

在上述例子中,常數three的值被用來建立一個Double型別的值,否則加號兩邊的數型別不同,無法相加。

浮點數轉換為整數的反向轉換也可以,整數型別可以用DoubleFloat型別來初始化。

let roundPi = Int(simplePi)  // roundPi等於3,所以被推斷為Int型別
print("浮點數轉換至整數:", roundPi)
---
output: 浮點數轉換至整數: 3

用這種方式初始化新整數值時,浮點值會被截斷,也就是4.75會變成4,-3.9會變成-3。

注意

結合數字常數與變數不同於結合數字字面值。字面值3可以直接與字面值0.14159相加,因為數字字面值本身沒有明確型別。它們的型別只在編譯器需要求值時才會被推斷。

型別別名

型別別名(Type Aliases)就是給現有型別定義另一個名稱。開發者可以使用typealias關鍵字來定義型別別名。

當想要給現有型別取一個更有意義的名稱時,型別別名非常有用。

假設現在正在處理特定長度的外部資源資料,定義型別別名後,可以在任何使用原始名稱的地方使用別名。

typealias AudioSample = UInt16
var maxAmplitudeFound = AudioSample.min  // maxAmplitudeFound現在是0
print("maxAmplitudeFound現在的值是:", maxAmplitudeFound)
---
output: maxAmplitudeFound現在的值是 0

上述例子中,AudioSample被定義為UInt16的別名,因為是別名,所以AudioSample.min其實就是UInt16.min

布林值

Swift有一個基本的布林(Boolean)型別,叫做Bool。布林值指的是邏輯上的值,只能為真或假。Swift有兩個布林值常數,truefalse

let orangesAreOrange = true
let turnipsAreDelicious = false

orangesAreOrangeturnipsAreDelicious的型別會被推斷為Bool,因為它們的初值是布林字面值。就像前面提到的IntDouble一樣,如果建立常數/變數時就給予truefalse,就不需要將其宣告為Bool型別。

撰寫條件語句,特別是if條件語句時,布林值就非常有用。

if turnipsAreDelicious {
        print("Haha, turnips are delicious!")
} else {
        print("Oh shit! Turnips are horrible!")
}
---
output: Oh shit! Turnips are horrible!

如果在需要Bool型別的地方使用非布林值,Swift的安全機制會報錯。

let test = 1
//if test {}  // 如果取消這行註解,編譯器會報錯:Type 'Int' cannot be used as a boolean; test for '!= 0' instead

同樣地,下例是合法的。

let i = 1
if i == 1 {}  // i == 1的結果是Bool型別,所以可以通過Swift的型別檢查

元組

元組(Tuple)將多個值組合成一個複合值。元組內的值可以是任意型別,不必相同。

下例,(404, “Not Found”) 是一個描述HTTP狀態碼的元組。

let http404Error = (404, "Not Found")  // http404Error的型別是(Int, String)

(404, “Not Found”)元組將一個Int值與一個String值組合起來,表示HTTP狀態碼的兩個部分。這個元組可以描述為「一個型別為(Int, String)的元組」。

可以將任意順序的型別組合成一個元組,元組可以包含所有型別。還可以將元組內容分解(Decompose)成單獨的常數與變數,然後就能正常使用它們。

let (statusCode, statusMessage) = http404Error
print("The http status code is \(statusCode).")
print("The http status message is \(statusMessage).")
---
output: The http status code is 404.
The http status message is Not Found.

如果只需要部分元組值,分解時可以將要忽略的部分用底線(_)標記,類似於Python

let (httpStatusCode, _) = http404Error
print("The temp http status code is \(httpStatusCode).")
---
output: The temp http status code is 404.

也可以透過下標存取元組中的單一元素,下標從零開始。

print("透過下標存取元組中的第一個元素:", http404Error.0)
print("透過下標存取元組中的第二個元素:\(http404Error.1)")
---
output: 透過下標存取元組中的第一個元素: 404
透過下標存取元組中的第二個元素:Not Found

可以在定義元組時給每個元素命名,然後透過名稱取得這些元素的值。

let http500Status = (statusCode: 500, description: "Server Error")
print("透過元組元素命名方式存取第一個元素:\(http500Status.statusCode)")
print("透過元組元素命名方式存取第二個元素:\(http500Status.description)")
---
output: 透過元組元素命名方式存取第一個元素:500
透過元組元素命名方式存取第二個元素:Server Error

當函式需要回傳多個值時,元組非常有用。


可選型別

使用可選型別(Optionals)來處理值可能不存在的情況。可選型別表示有兩種可能:

要嘛有值,開發者可以選擇存取這個值;要嘛根本沒有值

舉例來說,SwiftInt型別有一種建構子,可以將String值轉換成Int值。然而,不是所有字串都能轉換成整數。字串「123」可以轉換成數字123,但字串「Hello, world」就不行。

let possibleNumber = "123"
var convertedNumber = Int(possibleNumber)
print("convertedNumber被推斷為型別「Int?」,也就是「optional Int」:", convertedNumber!)
let possibleName = "Jensen"
convertedNumber = Int(possibleName)
print("字串\(possibleName)轉成數字後有問題:", convertedNumber)
---
output: convertedNumber被推斷為型別Int?」,也就是「optional Int」: 123
字串Jensen轉成數字後有問題 nil
---
warning: Expression implicitly coerced from 'Int?' to 'Any'

由於這個建構子可能會失敗,所以Int(xxx)回傳的是可選型別(optional)的Int,而不是一般的Int。可選的Int寫作Int?,問號代表這個值是可選型別,但不能包含其他型別的值,例如BoolString,只能是Int或什麼都沒有。

nil

可以給可選變數賦值為nil,表示沒有值。

var serverResponseCode: Int? = 404  // serverResponseCode包含一個可選的Int值404
serverResponseCode = nil  // serverResponseCode現在沒有值
print("可選值serverResponseCode:", serverResponseCode)
---
output: 可選值serverResponseCode nil
---
warning: Expression implicitly coerced from 'Int?' to 'Any'

請注意,nil不能用於非可選的常數與變數。如果程式中有常數或變數需要處理值不存在的情況,請將它們宣告為對應的可選型別。如果宣告一個可選常數或變數但沒有賦值,它們會自動被設定為nil

var surveyAnswer: String?
print("沒有賦值的可選常/變數的值為:", surveyAnswer)
---
output: 沒有賦值的可選常/變數的值為: nil
---
warning: Expression implicitly coerced from 'String?' to 'Any'

if語句與強制解析

可以使用if語句與nil比較來判斷可選值是否有值,可以用「等於」(==)或「不等於」(!=)來比較。

如果可選型別有值,它就不等於nil

if convertedNumber != nil {
        print("convertedNumber contains some integer value.")
} else {
        print("convertedNumber is nil.")
}
---
output: convertedNumber is nil.

當確定可選型別確實有值後,可以在可選名稱後加上驚嘆號!來取得值。這個動作稱為可選值的強制解析(Forced Unwrapping)。

let optionalInteger: Int? = 10086
if optionalInteger != nil {
        print("The optional integer value is \(optionalInteger!)")  // 取得不存在的可選值會導致執行錯誤,使用!強制解析前,一定要確定可選有非nil的值。
} else {
        print("The optional value is Nil.")
}
---
output: The optional integer value is 10086

可選綁定

可選綁定(Optional Binding)用來判斷可選型別是否有值,如果有就把值賦給一個暫時的常數或變數。可選綁定可以用在ifwhile語句中。

if let actualNumber = Int(possibleNumber) {
        print("'\(possibleNumber)' has an integer value of \(actualNumber).")
} else {
        print("'\(possibleNumber)' could not be converted to an Integer.")
}
---
output: '123' has an integer value of 123.

上述程式碼可以理解為「如果Int(possibleNumber)回傳的可選Int有值,建立一個叫actualNumber的新常數並將可選Int的值賦給它。actualNumber已經被可選型別的值初始化,所以不需要再用!來取得它的值」。

可以在同一個if語句中包含多個可選綁定或多個布林條件,只要用逗號分隔即可。只要有一個可選綁定的值為nil或任一布林條件為false,整個if條件就為false

if let firstNumber = Int("4"), let secondNumber = Int("42"), firstNumber < secondNumber && secondNumber < 100 {
        print("\(firstNumber) > \(secondNumber) < 100")
}
// 等同於下面的程式碼
if let firstNumber = Int("4") {
        if let secondNumber = Int("42") {
                if firstNumber < secondNumber && secondNumber < 100 {
                        print("\(firstNumber) > \(secondNumber) < 100")
                }
        }
}
---
output: 4 > 42 < 100
4 > 42 < 100

注意

在if條件語句中使用常數與變數建立可選綁定,只能在if語句的主體(body)中取得值。相對地,在guard語句中建立可選綁定,只能在guard語句外且在語句之後取得值。

隱式解析可選型別

有時候在程式架構中,第一次被賦值後,可以確定一個可選型別之後一定會有值。在這種情況下,每次都判斷與解析可選值會很沒有效率。這種型別的可選狀態稱為隱式解析可選型別(Implicitly Unwrapped Optionals)。將想要用作可選的型別後面的問號String?改成驚嘆號String!來宣告一個隱式解析可選型別。與其在使用時將驚嘆號放在可選型別名稱後面,不如在定義時就直接將驚嘆號放在型別後面。

當可選型別第一次被賦值後就能確定之後一定有值時,隱式解析可選型別非常有用。隱式解析可選型別主要用在Swift中類別的初始化過程。

一個隱式解析可選型別其實就是一般的可選型別,但可以當作非可選型別來使用,不需要每次都解析可選值。

let possibleString: String? = "An optional String."
let forcedString = possibleString!
print("顯式解析可選型別:", forcedString)

let assumedString: String! = "An implicitly unwrapped optional String."
let implicitString: String = assumedString
print("隱式解析可選型別:", implicitString)
---
output: 顯式解析可選型別: An optional String.
隱式解析可選型別: An implicitly unwrapped optional String.

可以把隱式解析可選型別當作一個可以自動解析的可選型別。當使用一個隱式解析可選值時,Swift會先把它當作一般可選值。如果不能當作可選型別使用,Swift會強制解析可選值。在上述程式碼中,可選值assumedString在賦值給implicitString前會被強制解析,因為implicitString本身型別就是非可選型別的String

下例optionalString沒有明確的資料型別,根據型別推斷,就是一般的可選型別。

let optionalString = assumedString

如果在隱式解析可選型別沒有值時嘗試取值,會觸發錯誤。與在沒有值的一般可選型別後加驚嘆號一樣。

可以將隱式解析可選型別當作一般可選型別來判斷是否有值。

if assumedString != nil {
        print("隱式解析可選型別取值:", assumedString!)
}
---
output: 隱式解析可選型別取值: An implicitly unwrapped optional String.

也可以在可選綁定中使用隱式解析可選型別來檢查並解析其值。

if let definiteString = assumedString {
        print("隱式解析可選型別取值:", definiteString)
}
---
output: 隱式解析可選型別取值: An implicitly unwrapped optional String.

注意

如果一個變數之後可能變成nil,請不要使用隱式解析可選型別。如果需要在變數生命週期中判斷是否為nil,請使用一般可選型別。


錯誤處理

可以使用錯誤處理(Error Handling)來應對程式執行中可能遇到的錯誤狀況。

相較於可選型別用值的有無來表達函式的成功與失敗,錯誤處理可以推斷失敗的原因,並傳遞到程式的其他部分。

當一個函式遇到錯誤狀況時,可以拋出錯誤。呼叫函式的地方可以捕捉錯誤訊息並合理處理。

func canThrowAnError() throws {
        // 這個函式內部可能會拋出錯誤
}

一個函式可以在宣告時加上throws關鍵字來拋出錯誤訊息。當函式可能拋出錯誤時,應在運算式前加上try關鍵字。

do {
        try canThrowAnError()
        // 沒有錯誤訊息拋出則執行後續程式
} catch {
        // 有錯誤訊息拋出則執行這裡
}

一個do語句會建立新的作用域,使錯誤能被傳遞到一個或多個catch子句。下例為錯誤處理如何應對不同錯誤狀況的範例。

func makeASandwich() throws {
        // function body
}

enum SandwichError: Error {
        case outOfCleanDishes
        case missingIngredients(ingredients: String)
}

do {
        try makeASandwich()
        // eatASandwich()
} catch SandwichError.outOfCleanDishes {
        // washDishes()
} catch SandwichError.missingIngredients(let ingredients) {
        // 如果匹配missingIngredients的錯誤被拋出,buyGroceries(_:)函式會被呼叫,並使用catch捕捉到的關聯值[ingredients]作為參數。
        // buyGroceries(ingredients)
}
---
warning: Immutable value 'ingredients' was never used; consider replacing with '_' or removing it

上述例子中,makeASandwich()(做三明治)函式會拋出錯誤訊息,如果沒有乾淨的盤子或某個原料缺失。因為makeASandwich()會拋出錯誤,函式呼叫要包在try運算式中。將函式包在do語句中,任何被拋出的錯誤都會被傳遞到提供的catch子句。


斷言與前置條件

斷言與前置條件是在執行時進行的檢查。可以用來檢查在執行後續程式前,某個必要條件是否被滿足。如果斷言或前置條件中的布林條件評估結果為true(真),則程式照常執行。如果評估結果為false(假),代表程式目前狀態無效,程式執行會終止,應用程式中止。

斷言幫助開發者在開發階段發現錯誤與不正確的假設,前置條件則幫助開發者在生產環境偵測問題。與錯誤處理不同,斷言與前置條件不是用來處理可恢復或可預期的錯誤。因為斷言失敗代表程式處於無效狀態,無法捕捉失敗的斷言。

注意

使用斷言與前置條件不是避免程式出現無效狀態的寫法。但如果真的出現無效狀態,斷言與前置條件可以強制檢查資料與程式狀態,使程式可預期地中止(不是系統強制、被動中止),並幫助問題更容易除錯。一旦偵測到無效狀態,執行就會中止,避免無效狀態造成系統進一步損害。

斷言與前置條件的差異在於它們何時進行狀態檢查:斷言只在除錯環境執行,前置條件則在除錯與生產環境都會執行。在生產環境中,斷言條件不會被評估。這代表可以在開發階段大量使用斷言,但這些斷言在生產環境不會有任何影響。

使用斷言進行除錯

可以呼叫Swift標準函式assert(_:_:file:line:)來寫斷言。傳入一個結果為truefalse的運算式與一段訊息,當運算式結果為false時這段訊息會顯示出來。

let age = -3
assert(age >= 0, "A person's age cannot be less than zero.")  // 因為age < 0,所以斷言會被觸發

上述程式碼中,如果age的值是負數,就像程式碼中那樣,age >= 0false,斷言被觸發,應用程式終止。

如果不需要斷言訊息,可以像下面這樣省略。

assert(age >= 0)

如果程式已經檢查過條件,可以使用assertionFailure(_:file:line:)函式來表示斷言失敗。

if age > 10 {
        print("You can ride the roller-coaster or the ferris wheel.")
} else if age > 0 {
        print("You can ride the ferris wheel.")
} else {
        assertionFailure("A person's age can't be less than zero.")
}

強制執行前置條件

當某個條件可能為假,但繼續執行程式要求條件必須為真時,需要使用前置條件。例如用前置條件檢查是否陣列越界,或檢查是否傳入正確參數給函式。

可以使用全域precondition(_:_:file:line:)函式來寫前置條件。傳入一個結果為truefalse的運算式與一段訊息,當運算式結果為false時這段訊息會顯示出來。

var index = 0
precondition(index > 0, "Index must be greater than zero.")  // 索引必須從1開始

可以呼叫preconditionFailure(_:file:line:)方法來表示出現錯誤,例如,switch進入default分支,但所有有效值應該被其他分支處理。

注意

如果用unchecked模式編譯程式,前置條件將不會檢查。編譯器假設所有前置條件都為true(真),會最佳化你的程式碼。但fatalError(_:file:line)函式無論如何都會中斷執行,不論最佳化設定如何。

可以在設計原型與早期開發階段使用fatalError(_:file:line)函式,此階段只有方法宣告但沒有實作,可以在方法體中寫fatalError(“Unimplemented”)作為實作。因為fatalError不會像斷言與前置條件那樣被最佳化掉,可以確保程式執行到未實作的方法時會中斷。

最後更新於