2. 基礎知識

這一章將說明Swift的基本語法架構與重要特徵,也是任何一個Swift程式幾乎都會需要的基礎知識。學習並掌握了這些基礎知識後,才有能力更進一步學習各種進階語法與高階功能。

2.1 變數與常數宣告


Swift的變數或常數在使用前必須要先宣告,如果是變數使用保留字var,常數則是使用let。Swift語言有大小寫之分,因此大小寫不同就代表不同的變數或常數。

var number = 10
var width = 0, height = 0
let pi = 3.14

使用var宣告的變數表示之後可以再修改內容,例如我們可以將number這個變數改為其他的值,而使用let宣告的常數只要一開始決定內容,之後不能再修改了,例如pi在初始化的時候就被指定為3.14,之後就無法再修改。順道一提,Swift已經內建了常數pi,透過Float.pi或Double.pi就可以取得,不需要再自己定義。

此外,若變數或常數在宣告的時候就給一個值,這個值稱為初始值,例如上述程式碼中的number、width、height與pi,都有一個初始值。有初始值的變數或常數可以在宣告時省略他們的資料型態,Swift的編譯器會自動根據初始值內容推論出適當的型態。例如變數number的初始值是10,編譯器會自動設定number的資料型態為Int整數型態,此外,變數width與height也是Int整數型態,但由於pi的初始值是3.14,因此編譯器會設定pi的資料型態為Double浮點數型態。資料型態(也稱資料型別)對於程式語言是非常重要的,因為資料型態決定該變數或常數具有哪些功能或哪些操作方式,例如數字型態(包含整數或小數)可以用來進行數學運算,而文字型態可以進行關鍵字搜尋。

除了在初始化的時候讓編譯器自動推論出資料型態外,我們也可以明確的指出變數或常數的資料型態,例如:

var number: Int = 10
var width: Int = 0, height: Int = 0
let pi: Double = 3.14

當編譯器的型態推論結果與我們需求不同時,這時就需要明確的指出資料型態,例如我們需要一個Double型態的變數,但初始值卻是0,這時候就要明確指出資料型態為Double了,否則會變成Int型態。另外還有一種方式可以讓編譯器動推論出Double型態,就是初始值設定為0.0。

var n: Double = 0

如果變數在宣告時已經明確指出資料型態,此時並不需要同時給初始值,之後需要的時候再給值就可以了,如下:

var number: Int
number = 20

命名規則

給變數或常數一個名字稱之為命名,例如上述的number、width、height或pi。命名有一些規定,違反這些規定的名字不建議或是不可以使用,如下:

  1. 系統已經使用的字原則上不可以使用,例如我們不應該將變數名稱取名為var。但如果一定要用,Swift也開了一個方便之門,只要在這些名稱前後加上backtick「`」符號即可,例如:
var `var`: Int
var = 10
  1. 名稱不可以是阿拉伯數字開頭,例如1person、10dollars、1000都是不允許的。
  2. 名稱必須是一個字,中間不可以有空白,例如school bus中間有個空白。
  3. 不可以使用特殊符號,例如「+」「-」「*」「/」「,」…等,但是底線「_」是允許的,並且底線也可以放在開頭位置,例如_school_bus,這樣命名是可以的。
  4. 第一個字建議小寫(這是命名慣例,非強制),如果是兩個以上單字組合而成的名字,除第一個字外,其他字開頭建議大寫(稱為駝峰命名法),例如firstNumber或schoolBus。
  5. 可以使用表情符號。表情符號也是合法字元,因此可以使用在變數或常數的名稱上。
  6. 可以使用中文。變數或常數可以使用中文命名,當然其他國家文字也可以。

2.2 輸出與輸入

函數print()稱為標準輸出函數,是常用的一個將文字輸出到終端機螢幕上或是Xcode除錯視窗中的函數。如果您現在使用Xcode的Playground專案來執行本書的Swift程式,變數或常數的內容就要使用print()來顯示。使用上並不困難,只要將變數或常數放到print()的括號中當成參數傳進去即可,例如:

var str = "Hello, playground"
print(str)

Swift允許在一個字串中直接嵌入變數或常數,然後再重新組合出一個新的字串,嵌入的語法為使用「()」,例如:

let name = "John"
print("Hi, (name)")
// Prints "Hi, John"

除此之外,兩字串合併可以使用符號「+」或是「,」來連結兩個字串,差別在於「+」不會在兩個字串中插入空白而「,」會,如下:

let name = "John"
print("Hi," + name)
// Prints "Hi,John"

print("Hi,", name)
// Prints "Hi, John"

使用「()」合併字串帶來的好處要比「+」或是「,」來的更多。以符號「+」而言,假設要輸出的變數或常數型態是整數而不是字串,例如下面程式碼中的常數result無法與字串”5 + 3 = “透過「+」合併,因為result目前的型態為Int,因此必須先將result的型態轉成String才行。

let result = 5 + 3
print("5 + 3 = " + result)
// Error: Binary operator '+' cannot be applied to operands of type 'String' and 'Int'

將符號「+」改為「,」試試。雖然使用「,」合併字串時,Swift會自動將數字型態轉成字串,但要寫出下面這樣的程式碼,非常容易出錯,當變數或常數太多的時候,程式碼也不容易閱讀。

let a = 5, b = 3
print(a, "+", b, "=", (a + b))
// Prints "5 + 3 = 8"

但若使用「()」來合併字串,整個程式碼就相對乾淨許多,也沒有型態不一致的問題,如下所示。

print("(a) + (b) = (a + b)")

print()輸出時會在結尾處自動加上換行符號,因此如果連續呼叫print()兩次時,第二次輸出的資料會在新的一行。若不希望換行,只要在print()函數中補上terminator參數標籤,然後再給terminator參數一個空字串(連續兩個雙引號)即可,空字串表示將結尾預設的換行符號以空字串取代,如下所示:

print("Today is a ", terminator: "")
print("good day")
// Prints "Today is a good day"

所以如果想要輸出一個CSV格式的字串,透過terminator參數也很容易做到,如下:

print("姓名", terminator: ",")
print("住址", terminator: ",")
print("電話")
// Prints "姓名,住址,電話"

輸入

readLine()是標準輸入函數,也就是從終端機的鍵盤輸入資料。只是這個函數在Playground專案中沒有效果,想要透過這個函數得到從鍵盤上輸入的資料,請建立macOS的Command Line Tool專案。

var name: String?
print("請輸入姓名:", terminator: "")
name = readLine()
print("Hi, (name!)")

~補充說明~

  1. 「String?」與「name!」與Optional型態有關,請參考本章第八節。
  2. readLine()函數對於App開發者而言並不重要,App的資料輸入是透過專門的資料輸入元件取得使用者輸入的資料。一般來說,readLine()只有終端機程式開發者才會經常使用。

2.3 註解與分號

註解有兩種形式:單行註解,使用「//」符號,代表從這個符號之後一直到該行結束都是註解,例如:

// 這是個註解

多行註解,使用「/」開頭與「/」結尾,例如:

/*
這裡面的內容
全部都是註解
*/

也支援巢狀註解,例如:

/*
這裡面的內容
全部都是註解
    /* 一樣是註解 */
*/

分號

分號「;」專門用來分隔原本屬於兩行的程式碼,例如:

var n: Int; n = 5 + 3

雖然將原本兩行程式碼寫在同一行,但不建議寫出這樣風格的程式碼,除了增加程式閱讀困難外,沒有什麼好處。此外,Swift可以省略每一行程式碼最後的分號,雖然加了也不會錯,但沒有必要性了。

2.4 基本資料型態

在Swift中所有的資料型態都是大寫開頭,例如整數Int,浮點數Double,布林Bool等。這些大寫型態代表本質上是結構struct或類別class,與C或是Objective-C使用小寫的int、double或是bool在功能上完全不同。小寫型態裡面只能單純的存放數值,而大寫型態除了數值外,還可以放函數,提供更多操作資料的方式,若嫌不足還可以自己擴充。目前Swift中已經沒有小寫的型態,所有資料型態都是大寫。

這一節介紹幾個常用的型態,包含有整數Int、小數Float與Double、布林Bool、字串String與二位元Data,並瞭解型態與型態間的轉換方式。其中字串型態功能非常多,會單獨成一章來討論,這裡先略微簡述。

整數

整數型態包含有號整數與無號整數(亦即正整數),有號整數可區分為Int、Int8、Int16、Int32與Int64;無號整數分為UInt、UInt8、UInt16、UInt32與UInt64。數字表示該型態用了多少bit來儲存資料,例如Int8表示用了8個bit,Int16就是用了16個bit。後面沒有帶數字的Int型態會自動根據作業系統位元數調整,例如32位元作業系統的Int就相當於Int32,64位元作業系統就相當於Int64。不同型態的整數能夠表示的數字大小是有範圍的,並且有號數的第一個bit被拿來當正負號使用,所以若以Int8為例,該型態的整數範圍為-27到27-1,也就是-128到127。UInt8沒有正負號bit,所以範圍為0~28-1,也就是0到255。整數型態可以使用屬性max與min來取得每個型態的數字範圍,例如:

print(Int64.max) // 9223372036854775807
print(Int64.min) // -9223372036854775808

整數型態還可以表示二進位、八進位與十六進位資料,只要在資料前面分別加上0b、0o與0x即可,舉例如下:

let bin: Int = 0b00000111 // 二進位
let oct: Int = 0o10       // 八進位
let hex: Int = 0xA        // 十六進位

前面提過大寫開頭的資料型態比小寫開頭的具有更多功能,除了max與min屬性外,以二進位資料為例,如果我們想要知道開頭有多少連續的0以及結尾有多少連續的0,可以使用屬性leadingZeroBitCount與trailingZeroBitCount得到,程式碼如下:

let bin: UInt8 = 0b00011100

print(bin.leadingZeroBitCount)
// Prints "3"
print(bin.trailingZeroBitCount)
// Prints "2"

再舉個例子,如果我們有一個計步器程式,今天已經走了1000步,想要知道再5000步後是幾步,過去我們使用 1000 + 5000 來計算,現在我們也可以呼叫Int型態中內建的advanced(by:)函數來計算,函數語法請參考第6章。

let currentSteps = 1000
let newSteps = currentSteps.advanced(by: 5000)
// newSteps == 6000

或想要知道距離10000步的目標還有多遠,可以使用distance(to:),當然也可以用減的,但這個函數讓「還有多遠」的概念更加明確:

let currentSteps = 1000
let moreSteps = currentSteps.distance(to: 10000)
// moreSteps == 9000

此外,想要得到一個整數型態的亂數,可以使用random(in:)函數,例如下面的程式碼會得到範圍為3~50的整數亂數。其中符號「…」或「..<」稱為範圍運算子,「m…n」表示m到n(包含m也包含n),若為「m..<n」表示範圍為m到n-1。

let n = Int.random(in: 3...50)

~補充說明~
設定亂數的範圍必須在該型態允許的範圍內,例如「UInt.random(in: -6…10)」會得到錯誤訊息,因為UInt為正整數型態,-6顯然不在範圍內。

浮點數

浮點數能夠表示小數數字,家族有Float、Float32、Float64、Float80與Double。其中Float與Float32一樣,都使用了32個bit來儲存資料,Float64與Double一樣,使用了64個bit,而Float80則是使用了80個bit。浮點數沒有min與max屬性可以知道最大與最小範圍,因此這裡使用屬性pi當成例子印出圓週率,看看不同的浮點數型態能夠顯示出幾位數字,如下:

print(Float.pi)
// 3.1415925
print(Float32.pi)
// 3.1415925
print(Float64.pi)
// 3.141592653589793
print(Float80.pi)
// 3.1415926535897932385
print(Double.pi)
// 3.141592653589793

浮點數型態最常用需要的功能應該算是去小數了,而其中以四捨五入最為常用,rounded()函數用來將小數第一位四捨五入。

var n = 2.9
print(n.rounded())
// Prints "3.0"

函數rounded有兩種形式,沒有參數的rounded是四捨五入去小數,有參數的rounded可以根據參數內容選擇不同的去小數方式,例如無條件進位或無條件捨去。下面程式碼rounded中的參數.up為無條件進位,.down為無條件捨去。.up與.down語法請參考第11章列舉。

print(4.0001.rounded(.up))
// Prints "5.0"
print(2.998.rounded(.down))
// Prints "2.0"

~補充說明~
rounded()函數沒有提供留下小數幾位的功能,這部分要自己處理,例如要留下小數兩位,可以先乘上100,四捨五入後再除100。

布林

布林型態為Bool,只有兩種數值可以放入此型態的變數或常數中,分別是true與false。布林型態可以用來描述兩種不同的狀態,例如開關是開的還是關的,硬幣是正面還是反面,杯子是空的還是有裝東西。布林型態也是邏輯運算的結果,例如某個數字有沒有大於5,結果不是有大於5,不然就是沒有大於5,不會有第三種狀態,所以邏輯運算的結果也是布林型態。

由於布林型態只有兩種值,所以如果「not」該值就會得到另外一種狀態,例如「not」 true就會得到false,「not」 false就會得到true。下面的程式碼中,變數flag初始化為true,雖然沒有指出flag的型態,但藉由true這個值編譯器會自動推論出flag型態為Bool。符號「!」代表not的意思,也就是將flag的值轉成另外一個值,所以如果原本是true,!true就變成false,反過來說,!false就會變成true。

var flag = true
flag = !flag
// flag == false

~補充說明~
變數flag也稱為旗標變數,用來表示狀態,就像輪船間透過旗號來告知對方目前的船隻狀態一樣,不同的旗號代表不同的狀態。所以旗標變數就是用來記錄或改變目前程式運行的狀態。旗標變數不一定只能布林型態,但通常是布林型態。

字串

字串型態為String,在前面一些範例中已經看過這個型態。只要前後用雙引號包圍起來的資料就屬於字串,例如 “Swift is a powerful language”。字串中可以加入跳脫字元「」讓字串輸出時有些變化,例如「n」代表了換行,如下:

print("第一行文字n第二行文字")
// 第一行文字
// 第二行文字

除了n外,t代表「TAB」,或是\代表輸出一個。

字串不一定只能使用雙引號,連續三個雙引號包圍住的字串,代表多行文字字串,例如:

let str = """
第一行文字
第二行文字
第三行文字
"""

如果要將兩個字串合併,使用加號「+」,例如:

let a = "abc"
let b = "def"
let c = a + b
// c == "abcdef"

要判斷兩個字串內容是否相等,透過if語句以及使用兩個等號「==」即可。if語法請參考第3章流程控制。

let a = "abc"
let b = "abc"
if a == b {
    print("字串內容一樣")
}

上述程式碼中,常數a與常數b是相等的,這裡相等的意思是字串內容完全一致,所以a == b的邏輯運算結果會是true。

二位元

二位元型態為Data,這個型態中可儲存的資料包含文字型與非文字型資料。所謂的文字型資料指的是String型態中的資料。但有些資料並不是文字,這些資料沒有文字編碼(例如Unicode編碼),例如jpeg圖檔、mp3音樂或mov影像…等,我們用一般的文字編輯器開啟這些檔案時,只會看到一堆亂碼,這種資料就屬於二位元資料,英文稱為binary data。

二位元資料必須使用Data型態來儲存,如果把二位元資料轉成String型態,資料可能就損毀了。但反過來沒有問題,因Data能夠描述的資料範圍比String大,所以我們可以把String型態的資料轉成Data型態,不會產生問題。

Data型態在實際應用中很常見,例如網路資料傳輸、藍牙資料傳輸、各種影像、聲音資料處理都是Data型態。這裡我們就先知道資料分為兩種類型:文字型態text與二位元binary。然後文字型態的資料可以儲存在String型態與Data型態的變數或常數中,二位元資料只能儲存於Data型態的變數或常數中。

察看屬於何種資料型態

要想要知道資料屬於哪種資料型態,可以使用標準函數庫中的type(of:)函數,呼叫此函數後會傳回所屬資料型態,例如:

let n = 20
print(type(of: n))
// Prints "Int"

let str = "hello"
print(type(of: str))
// Prints "String"

或者用「is」,例如:

let n = 20.0

if n is Int {
    print("n為整數")
}
if n is Double {
    print("n為浮點數double")
}
// Prints "n為浮點數double"

~補充說明~
以上這段程式碼會得到型態檢查器發出的黃色警告訊息,因為n已經很明確可以被推斷出屬於Double型態,因此接下來的兩個if判斷沒有什麼意義,第一個if判斷一定是false,而第二個if判斷一定是true。「is」通常使用於Any或AnyObject型態以及類別,請參考第4章聚集型態與第8章結構與類別。

2.5 型態轉換

Swift是一個型態安全(type-safe)語言,代表了程式在編譯前會進行型態檢查,通過了才會開始編譯。型態檢查會確保數值存入變數或是常數時,數值的型態與變數或常數宣告時的型態一致。例如將數字10存入一個型態為String的變數中,或是將4.3存入Int型態的變數中都會得到型態有關的錯誤訊息,這在程式執行前就已經檢查完畢了。除此之外,Swift的型態檢查還會檢查運算式中的資料型態是否一致,如果不一致也會得到錯誤訊息,例如下述程式碼的型態檢查是失敗的,即便a與b都是整數型態,但對Swift而言,Int與Int64是兩個不同的型態,不同型態的資料是不可以放在一起運算的。

let a: Int = 10
let b: Int64 = 20
let result = a + b
// Binary operator '+' cannot be applied to operands of type 'Int' and 'Int64'

不同型態的資料要放在一起運算必須先將型態轉成一致的型態,這就是型態轉換。例如字串的”10″轉成整數10,這種轉換是被允許的,但反過來就不一定,例如我們不能把中文字轉成整數,這是沒有辦法轉換的。字串與數字間的轉換很常見,因為有時候取得的資料是字串形式,這時我們必須自己轉成其他形式才能做後續的處理。

整數轉字串

let n = 10
let str = String(n)

字串轉整數

let str = "30"
let n = Int(str)

整數轉浮點數

let n = 30
let f = Double(n)

浮點數轉整數(相當於去小數)

let f = 10.92
let n = Int(f)
// n == 10

其他未列出的型態轉換,例如Int轉Int64、Float轉Double、String轉Int64就不一一列舉,請讀者舉一反三了。

數字轉布林

非0數字可以轉換成true,數字0轉換成false。字串只有”true”或”false”可以轉成布林,其他無法轉換,即使是”1″或”0″也不行。

let b1 = Bool(truncating: 1)
// b1 == true
let b2 = Bool(truncating: 0.3)
// b2 == true
let b3 = Bool(truncating: 0)
// b3 == false
let b4 = Bool("true")
// b4 == true

布林轉數字與字串

true轉成數字後一律是1,false轉成數字後一律是0。true轉成字串為”true”,false轉成字串為”false”。

let n1 = Int(truncating: true)
// n1 = 1
let n2 = Double(truncating: true)
// n2 == 1
let s1 = String(true)
// s1 == "true"

String轉Data

轉換時必須要加上編碼資訊,如果原本的字串是用UTF-8編碼的,轉換時的參數值就要填入.utf8。

let str = "Hello World"
let data = str.data(using: .utf8)

Data轉String

如果Data中的資料可以轉成字串,一樣要輸入編碼格式。

let s = String(data: data!, encoding: .utf8)

Data與Base64編碼

有些Data格式的資料是無法轉成字串的,例如mp3、jpeg或是video格式。但有的時候我們又需要將他們轉成字串格式,例如想在JSON字串中包含圖片資料,這時候Base64編碼就很適合了。

var data = Data()
let str = data.base64EncodedString()

~補充說明~
上述程式碼變數data是空的,這僅是為了說明方便,日後如果data實際儲存了二位元資料,就可以透過base64EncodedString()轉成字串了。

完整的程式碼如下,但這段程式碼使用許多現在還沒看過的語法與函數,所以建議可以等到熟悉相關的語法與函數後再來看這段程式碼不遲。這段程式碼需要建立macOS的Command Line Tool專案才可以執行。

import Foundation

do {
    if let f = FileHandle(forReadingAtPath: "my.jpg") {
        if let data = try f.readToEnd() {
            let str = data.base64EncodedString()
            print(str) // <= base64 編碼的 my.jpg 圖檔
        }
        try f.close()
    }
} catch {
    print(error)
}

Base64字串轉Data

下面程式碼變數str中的字串是一個2 x 2解析度的jpeg圖檔經由base64編碼後的內容,如果再用base64解碼,就可以得到原本的jpeg圖檔了,解碼後的資料格式當然是Data型態。

var str = "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAIBAQEBAQIBAQECAgICAgQDAgICAgUEBAMEBgUGBgYFBgYGBwkIBgcJBwYGCAsICQoKCgoKBggLDAsKDAkKCgr/wAALCAACAAIBAREA/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/9oACAEBAAA/APoTVPBfg6w1O4sbHwnpkMEM7xwwxWEapGgYgKoAwAAMACv/2Q=="

let data = Data(base64Encoded: str)

2.6 型態別名

型態別名用來為現有的資料型態取一個別名,讓型態能夠更清楚的表明其用途。例如儲存學生成績的變數,雖然適合的資料型態為UInt8,但是UInt8可以表示很多的意思,如果我們使用型態別名為UInt8取一個Score這樣的名字,這樣我們一看就知道這個變數是用來儲存成績的。

typealias Score = UInt8
var score: Score = 90

如果要將某個整數型態的值給變數score,會出現錯誤訊息,這是因為Swift的型態檢查會以型態名稱為檢查依據,所以只要型態不是Score檢查都不會通過,例如下面的程式碼會得到錯誤訊息:

var value = 70
score = value
// Cannot assign value of type 'Int' to type 'Score' (aka 'UInt8')

型態別名並不會改掉原本的型態名稱,只是為原本的型態再增加另外一個名字而已。

2.7 Tuple

Tuple(有些中文翻譯為元組)是用小括號將兩個以上資料組合在一起的一種組合型資料型態。例如長與寬這兩種資料在使用時往往放在一起使用,我們就可以用tuple把他們合在一起。例如:

var size: (Int, Int) = (0, 0)

上述程式碼中的資料型態(Int, Int)代表了tuple,這個tuple包含了兩個Int型態的資料,由於編譯器可以透過等號右邊初始化的狀況而推斷出size的型態,因此(Int, Int)可以省略,省略後的程式碼如下:

var size = (0, 0)

使用時,第一個值位於tuple索引值0的位置,第二個值位於索引值1的位置,以此類推。tuple可以組合兩個以上的數值資料,所以要修改tuple中的內容可以透過tuple索引值或是用一個新的tuple直接覆蓋過去。

size = (640, 780)
size.0 = 1024
size.1 = 768

上面變數size這樣的設計,使用者並不知道tuple中第一個值是用來表示width還是height,因此,我們可以為tuple中的各部分設定一個標籤名稱,之後在存取個別元素時就可以透過標籤名稱來存取了。

var size: (width: Int, height: Int)
size.width = 640
size.height = 768

我們也可以宣告與tuple中數量完全一致的變數或常數來快速取出tuple中的每一個值,例如下面的(w, h):

var size: (width: Int, height: Int)
size = (1024, 768)
var (w, h) = size
// w == 1024, h == 768

上述程式碼中的變數w與h分別是1024與768。如果有哪個部分不是我們需要的,就可以用底線「_」取代,例如下述的程式碼只會將tuple中的width部分放入w中,至於height就忽略不管了。「_」是一個萬用字元有「don’t care」的意思。

var (w, _) = size
// w == 1024

2.8 Optional型態

Optional是Swift中非常特殊的一種資料型態,代表了每種現有的資料型態是否可以儲存nil這個值。Optional並不是獨立的資料型態,他必須與現有的資料型態一起使用。先以Int型態舉個例子,變數x的資料型態是Int,然後將一個nil值指定給這個變數。如果編譯器的型態檢查發現nil被指定給這個Int型態的變數,我們馬上就會得到錯誤訊息,如果程式已經執行起來才出現這個情形,程式立刻就當掉。

var x: Int
x = nil
// 'nil' cannot be assigned to type 'Int'

在Swift中,nil代表了「空」的意思,相當於C語言的NULL。產生nil的原因很多,通常是變數或常數實體化時還不知道要給什麼初始值,所以就先給nil,或是某個函數執行錯誤傳回了nil。當變數或常數內容為nil時,不可以對nil進行後續的運算,因為只要對nil進行運算必定導致程式當掉。在C或是Objective-C中,程式設計師必須對每一個有可能出現NULL或nil的變數進行檢查才能確保程式穩定運作,但這實在耗時耗力,所以大部分的程式設計師會根據經驗來判斷是否會出現NULL然後決定要不要檢查。Swift的作法是不依賴程式設計師經驗,改用了很聰明的方式來告訴程式設計師「這裡會出現nil」,然後讓程式設計師自行決定要怎麼處理。

問題解決要從源頭開始。我們從會收到nil的變數或常數開始處理nil問題。在Swift中,如果一個變數或常數可能收到nil,該變數或常數的資料型態必須宣告為Optional型態。但因為不論哪一個型態的變數或常數都有可能遇到nil,因此Optional型態不是一個獨立的資料型態,他得與現有型態一起配合才行。Optional型態就是在現有型態後方加上問號「?」後,該資料型態的變數或常數就可以儲存nil。例如:

var x: Int?
x = nil

Swift的所有型態都可以加上問號而形成Optional型態,包含tuple,例如下面size中儲存的各種資料都是合法的。

var size: (Int?, Int?)?
size = nil
size = (nil, 10)
size = (5, nil)
size = (nil, nil)

到這裡我們可以知道,資料型態如果加上問號就可以儲存nil,如果沒有加上問號就不可以儲存nil。

我們來看加了問號的資料型態到底造成了什麼樣的效果?用下面的圖來說明。最左邊圖的變數x資料型態為Int並且初始化為10,因此x的內容儲存了10。中間圖的資料型態為Int?,所以此Optional型態的變數會被分為兩部分,白色橢圓形專門儲存非nil值,所以10會放在白色橢圓形的位置。若變數y的值為nil,此時會如最右邊的圖,nil會儲存在灰色橢圓形裡面。從圖上可以知道,nil與非nil值在Optional型態中會放在不同的地方,並且這兩個位置是互斥的。白色橢圓內有值,灰色橢圓內就沒有nil,反之灰色橢圓內有nil,白色橢圓內就沒有值。

現在我們要把值從變數x與變數y中取出。如果是變數x,直接取出即可。如果是變數y,當我們將y的值給另外一個變數例如z時,z取得的不是10,而是整個y的形式,裡面包含了兩個橢圓形。所以當我們用print()函數印出z時會得到Optional(10)而不是單純的10,印出y的結果也是一樣。

var y: Int? = 10
let z = y
print(z)
// Prints "Optional(10)"

這種具有Optional型態的變數或常數,要取得其中白色橢圓的部分,必須要在變數或常數後方加上驚嘆號「!」,這樣編譯器就知道是要取白色橢圓的內容,也就是非nil的部分,這個動作稱之為「unwrap」。

var y: Int? = 10
let z = y!
print(z)
// Prints "10"

如果變數y的內容為nil,也就是圖右的狀況,此時「y!」就會導致程式當掉,因為y!是要取白色橢圓的內容,而白色橢圓不可能出現nil,所以y!就當掉了。因此要取得灰色橢圓的內容,也就是nil,變數後方不用加任何符號,就原本的變數即可,意思是如果print(y)看到的結果就是nil。

經由這樣的機制,我們知道如果變數或常數的資料型態屬於Optional型態,在取得內容時必須在後方加上「!」,但是加之前一定要先確認內容是不是nil,否則如果是nil,程式就當了。Swift提供了許多檢測與確保變數內容不是nil的方式,這邊我們先用條件判斷來檢測就可以了,其他的方式請參考第3章流程控制。

if y == nil {
    // y 的內容為 nil
} else {
    // y 的內容不為 nil
}

nil 與 ??

Optional型態可以使用「??」來設定當值為nil時的預設值。例如下述程式碼中的字串轉數字會失敗,因為字串”abc”無法轉成數字,因此Int(str)會傳回nil。如果是nil時就以0取代,因此n最後得到0。由於已經使用了「??」,所以n的內容不可能為nil,這時n的資料型態是Int而不是Int?。

var str: String = "abc"
let n = Int(str) ?? 0
// n == 0

隱性解開

當資料型態後方的「?」改為「!」時,稱為隱性解開(Implicitly Unwrapped Optional),看字面意思不太容易解釋,我們用例子來解釋何謂隱性解開。下方的變數x為Optional型態,並且因為沒有給nil以外的初始值,所以nil會成為Optional型態的初始值,換句話說,此時x的內容為nil。

var x: Int?

假設要將x加1,當然必須在非nil的情況下才可以進行,所以要先將x給一個非nil的值,然後加1時要在x後面加上「!」取非nil部分。

x = 10
x = x! + 1

如果已經可以確定運算時x不是nil,像上面這段程式碼,x已經是10了,我們很確定x不是nil,這時候x! + 1的「!」就顯的有點多餘,也讓程式碼看起來凌亂。既然x已經確定不會是nil,此時可以很放心的將x!的值交給另外一個變數,然後用那一個變數來運算,例如:

var z = x!
z = z + 1

雖然程式碼變乾淨了,但是要多宣告一個變數,既然我們已經確定x在運算前一定不會是nil,那乾脆一開始就將驚嘆號放在資料型態後方,這樣連變數z都可以不用宣告,因為Swift會自動幫我們取出x中非nil值。這就是隱性解開,意思就是Swift在背後自動幫我們取出x中非nil部分的值了。

var x: Int!
x = 10
x = x + 1

由於在一些情況下,變數宣告時無法初始化,但Swift又規定變數一定要初始化才可以,這時就需要將該變數宣告為Optional型態,這樣就可以先初始化為nil。但之後運算時又不想加上一堆的「!」,這時隱性解開就可以讓我們同時保有變數可以儲存nil能力,而運算時又會自動解開的方便性。但這個方便性的缺點就是我們不容易看出變數是Optional型態而導致忘了檢查nil,所以除非很有把握,建議還是不要使用隱性解開比較好。

前面說過,問題解決從源頭開始。如果變數x屬於Optional型態,而變數y為非Optional型態,這時將x指定給y的時候就會出現型態錯誤的訊息,因為等號兩邊的型態不一致。例如:

var x: Int? = 0
var y: Int
y = x
// Value of optional type 'Int?' must be unwrapped to a value of type 'Int'

這時有三種作法可以通過型態檢查,一個是將y的型態改為「Int?」,然後接下來y就會伴隨著「y!」來隨時提醒我們,y的內容有可能是nil需特別留意。例如:

var x: Int? = 0
var y: Int?
y = x

另外一種則是保證y從此之後不會出現nil,因此使用x!將非nil部分交給y,例如:

var x: Int? = 0
var y: Int
y = x!

最後一種就是隱性解開,如下:

var x: Int? = 0
var y: Int!
y = x

Swift標準函數庫中的許多函數在運算完後的傳回值型態屬於Optional型態,代表如果運算結果順利的話會收到正確的值,如果運算結果出問題會傳回nil。這時宣告一個變數或常數去接這種函數的傳回值時,資料型態也必須使用Optional型態。請看下面這個將字串轉成數字的例子。這段程式碼會得到一個錯誤訊息,因為字串轉數字時可能會失敗,所以Int的初始化器會傳回Optional型態的值,所以變數n的資料型態要改為「Int?」才正確。

var str: String = ""
var n: Int?
n = Int(str)

Swift透過Optional這一套機制大幅提昇程式品質與增加運作時的穩定度,因為在這套機制下會不斷的透過「?」與「!」來告訴我們:這裡可能會出現nil,請小心處理。

3 thoughts on “2. 基礎知識

  1. Archangel Wu
    Archangel Wu says:

    報告,在實作時:

    let s = String(data: data, encoding: .utf8) 會出現錯誤,要改成:
    let s = String(data: data!, encoding: .utf8) 就可以了。

    如果要 print 出來,print(s!) 也要加上 !

    • 朱克剛
      朱克剛 says:

      已加上「!」,謝謝告知。另外,print 中的 s 這裡建議不要加上 !,因為 data 不一定能成功轉為 String,如果轉不過去時 s 會是 nil,加上「!」後會當掉。

發表迴響