8. 結構與類別

Swift中有四種資料型態在定義的時候需要給個名字,稱為有名型態,分別是結構、類別、協定與列舉。有名型態也相當於自訂型態,具備了物件導向的封裝特性,因此構成了Swift最基礎的資料型態,其他常見的Int、Float、String、Array…等型態都是由這四個有名型態發展出來的型態。這一章將介紹結構與類別,協定與列舉在之後章節介紹。

8.1 定義與產生實體

結構與類別是最常見到的兩種資料型態,像是Int、Float、String這些常見的資料型態都是結構,在App中常見的各種視覺化元件,例如Button、Label、View都屬於類別。結構與類別都是一種資料型態,因此在命名的時候,名稱第一個字母應該大寫,定義兩者的語法如下:

// 結構
struct SomeStruct {
    // 定義屬性與方法
}

// 類別
class SomeClass {
    // 定義屬性與方法
}

若不考慮繼承的話,結構與類別能做的事情幾乎一樣,都可以定義屬性也都可以實作方法,在一般情況下,使用時也分不出來目前的型態是結構還是類別,除非去查原始定義。所以先以結構舉個例子。定義一個名稱為Animal的結構,其中有一個屬性name用來儲存動物名字,並且使用String的Optional型態,讓屬性值先初始化為nil,如下:

struct Animal {
    var name: String?
}

不論是結構還是類別,在大部分情況下,存取屬性或是呼叫方法都必須先產生結構或類別的實體(instance)。實體的意思是作業系統會根據結構或類別的定義配置一塊記憶體,裡面存放該類別或結構屬性與方法,這過程稱為實體化。同一個結構或類別可以產生很多個實體,每個實體都有獨立的記憶體空間,彼此不會互相干擾。結構產生實體的方式有兩種,第一種是在產生實體的同時就填入結構中所有屬性值,另一種是產生實體後再修改屬性值。下面程式碼中的常數lion屬於產生實體的同時就初始化屬性值,且之後無法再修改屬性值,因為lion是常數。變數elephant則是產生實體後再去修改屬性值,因為elephant是變數,所以可以在結構實體化後再修改屬性值。

let lion = Animal(name: "獅子")
var elephant = Animal()
elephant.name = "大象"

現在我們把結構改為類別,只要將struct換成class就好了,其他不變,如下:

class Animal {
    var name: String?
}

現在這個類別產生實體的方式只有一種,如下:

var lion = Animal()
var elephant = Animal()
lion.name = "獅子"
elephant.name = "大象"

類別不像結構可以在產生實體時就填入屬性值,因為類別預設的初始化器並不接受參數,所以產生實體時,類別名稱後方的小括號內不可以放任何參數。如果類別想要跟結構一樣,在產生實體時就可以透過參數輸入初始化值,必須自行定義初始化器,這部分稍後會說明。下面程式碼對沒有自行定義初始化器的類別而言,語法是錯誤的。

let lion = Animal(name: "獅子")
// error: argument passed to call that takes no arguments

8.2 Value Type與Reference Type

結構屬於value type,類別屬於reference type。Value type的意思是當我們把實體存入一個變數或常數時,該變數或常數包含了整個實體,也就是說實體所在的記憶體區塊就是該變數或常數,如下圖左。Reference type的意思是儲存實體的變數或常數的內容是並不是整個實體,而是「指向該實體所在的記憶體區塊」,如下圖右。所以隨著結構中的屬性或是方法數量改變,變數或常數所佔用的記憶體區塊大小是會變化的。但如果是類別,不論類別中的屬性或是方法數量如何改變,指向該類別實體的變數或常數所佔用的記憶體區塊大小都固定不變。

以下面這一小段程式碼為例,根據結構Animal先產生一個elephant實體,然後再將這個實體指定給另外一個變數anotherElephant。

struct Animal {
    var name: String?
}
var elephant = Animal()
var anotherElephant = elephant

由於結構屬於value type,因此當elephant將自身內容指定給anotherElephant時,這時候elephant所佔用的記憶體區塊會複製一份給anotherElephant,換句話說,anotherElephant與elephant是兩個不同的實體,所佔用的記憶體區塊是不同的。

此時如果修改了anotherElephant中的屬性值,並不會影響elephant中原本的內容。從下面這段程式碼很清楚可以看到,當我們修改了anotherElephant中的屬性name,然後檢查elephant中的屬性name發現,elephant的name沒有因為anotherElephant的修改而被改掉,所以證明了elephant與anotherElephant是兩個獨立的實體。

190
var elephant = Animal()
elephant.name = "米米"
var anotherElephant = elephant
anotherElephant.name = "可可"
print(elephant.name!)
// Prints "米米"
print(anotherElephant.name!)
// Prints "可可"

但類別不一樣,類別屬於reference type,anotherElephant與elephant內容會指向同一個記憶體區塊,因此如果從anotherElephant中去修改了屬性值,原本elephant中的屬性值也就改掉了。把struct改成class再試試看。

class Animal {
    var name: String?
}

...
    
print(elephant.name!)
// Prints "可可"
print(anotherElephant.name!)
// Prints "可可"

由於類別是reference type,有時候我們會需要知道兩個變數或常數是否指向同一個記憶體區塊,也就是指向同一個實體的意思,這時可以使用「===」運算子來判斷,這個運算子只能用在類別型態,例如下面的程式碼elephant1與elephant2的內容是同一個實體,而elephant3則是另外一個實體,與elephant1或elephant2都不同。

class Animal {
    var name: String?
}

var elephant1 = Animal()
var elephant2 = elephant1
var elephant3 = Animal()

print(elephant1 === elephant2)
// Prints "true"
print(elephant1 === elephant3)
// Prints "false"

從上面這些以及之後陸續會接觸到的例子來看,結構與類別提供的功能非常相似,除了類別可以繼承另外一個類別之外,在很多情況下兩者都可以使用。要如何決定什麼時候該使用結構,什麼時候該使用類別呢?建議是:預設使用結構,除非有明確使用類別的需求,不然應該使用結構。

8.3 再論類別

類別與結構都具有封裝的特性,封裝的意思是將該類別或結構所有相關的特徵(即屬性)與操作方式(即方法或函數)全部定義在類別或結構裡面。但類別與結構最大的不同處,就是類別具有繼承能力而結構沒有。繼承代表類別中的屬性或是方法並不一定要自己定義,透過繼承他們可以來自於另外一個類別。先來看一個簡單的例子,想像一下我們要打造一個遊樂場,遊樂場裡面有雲霄飛車設施,然後我們要給遊樂場一個響亮的名字,這個遊樂場的藍圖(也就是類別定義)如下,其中的init(name:)稱為初始化器,在類別實體化時會呼叫。初始化器在本章稍後會說明。

class Playground {
    var name: String
    
    init(name: String) {
        self.name = name
    }
    
    func buildRollerCoaster () {
        print("\(name)建立了雲霄飛車")
    }
}

現在我們要根據Playground藍圖產生實際的遊樂場,程式碼如下:

let valley = Playground(name: "森林谷")
valley.buildRollerCoaster()
// Prints "森林谷建立了雲霄飛車"

但這個遊樂場似乎陽春了點。現在想像一下,我們想要打造一個比Playground更豪華的遊樂場,除了雲霄飛車外還想要加上摩天輪,這時有兩種作法:一種是重新設計一個新的;另外一種是根據Playground現有設施再加上摩天輪就好。我們選擇後者。這時要做的事情就是設計一個新的具有摩天輪的類別然後繼承Playground,如下:

class MoreFun: Playground {
    func buildFerrisWheel() {
        print("\(name)建立了摩天輪")
    }
}

繼承的語法使用冒號「:」,冒號後方就是繼承的類別名稱,因此「MoreFun: Playground」我們稱為「類別MoreFun繼承Playground」。MoreFun稱為Playground的子類別,而Playground稱為MoreFun的父類別。有些程式語言可以使用多重繼承,也就是同時繼承兩個以上父類別,但是Swift是單一繼承,一次只能繼承一個類別。在預設狀況下,Playground所有的屬性與方法都會繼承給MoreFun,因此,我們才能在建立MoreFun實體時給遊樂場名字,並且呼叫建立雲霄飛車以及摩天輪的方法。透過繼承,我們可以節省大量的程式碼,只要在原有的基礎上繼續疊加即可。

let desert = MoreFun(name: "乾燥沙漠")
desert.buildFerrisWheel()
// Prints "乾燥沙漠建立了摩天輪"
desert.buildRollerCoaster()
// Prints "乾燥沙漠建立了雲霄飛車"

所以現在我們有兩套遊樂場藍圖了,一套比較陽春只有雲霄飛車,另一套豐富一點除了雲霄飛車外還有摩天輪。我們還可以繼續再設計一個更豪華的遊樂場,繼承MoreFun或者Playground,最後我們就有三份遊樂場藍圖了,可以根據城市大小,經費預算以及遊客需求來決定要使用哪一份藍圖建立適合的遊樂場。

除了使用繼承的方式可以增加類別功能外,另外還可以使用擴充extension的方式還增加類別功能。請參考本章擴充一節。

多型與覆寫

多型(Polymorphism)是物件導向中重要的概念,代表一個實體可以同時有很多不同的型態,各個型態又提供了統一的操作界面,所以當這個實體在取得操作界面時必須先確定目前是哪一個型態。在物件導向實作上,多型跟繼承有密切關係,子類別實體化時的資料型態除了子類別外,也同時可以是父類別。例如在MoreFun遊樂場中我們想要打造一個跟Playground不一樣的雲霄飛車,於是重新改寫父類別的buildRollerCoaster(),但改寫後的方法名稱、參數與傳回值型態必須與父類別完全一樣,只有程式碼內容部分可以不同,這樣稱為覆寫,也就是子類別重寫父類別的方法。覆寫的方法必須在func前面加上override修飾子,明確告訴編譯器,這個方法在父類別也有一個一模一樣的,但現在要改寫他,如下:

class MoreFun: Playground {
    func buildFerrisWheel() {
        print("\(name)建立了摩天輪")
    }
    
    override func buildRollerCoaster () {
        print("\(name)建立了360度雲霄飛車")
    }
}

此時根據Playground或MoreFun所產生出來的實體,在呼叫buildRollerCoaster()方法時自然會對映到他們自己所屬型態中的方法,如下:

let valley = Playground(name: "森林谷")
let desert = MoreFun(name: "乾燥沙漠")
valley.buildRollerCoaster()
// Prints "森林谷建立了雲霄飛車"
desert.buildRollerCoaster()
// Prints "乾燥沙漠建立了360度雲霄飛車"

概念上很容易理解, 類別A的實體呼叫類別A中的方法,類別B的實體呼叫類別B中的方法,即使A與B有繼承關係也是一樣,這是很清楚的。我們再看下面這個例子,如果額外寫了一個建立遊樂場的函數buildPlayground(_:),只要呼叫這個函數就可以把一整座遊樂場建立起來,我們看這個函數如何設計。假設先只建立雲霄飛車。

func buildPlayground(_ playground: Playground) {
    playground.buildRollerCoaster()
}

注意上述函數中的參數型態為「所有遊樂場藍圖的父類別」也就是Playground,這時任何繼承Playground類別的實體都可以當成參數傳進來,而在此函數中所呼叫的buildRollerCoaster()會自動根據傳進來參數的原本型態呼叫正確的方法,而不是永遠只呼叫Playground中的方法。這是多型很重要的一個特性,我們不需要為不同的遊樂場藍圖撰寫不同的建立遊樂場函數,現在只要寫一個就好了。

buildPlayground(valley)
// Prints "森林谷建立了雲霄飛車"
buildPlayground(desert)
// Prints "乾燥沙漠建立了360度雲霄飛車"

接下來我們把這個函數完成。由於MoreFun可以建立摩天輪而Playground沒有,所以我們必須在buildPlayground(_:)中加上判斷式,如果傳進來的參數實際型態是MoreFun,就必須建立摩天輪。我們可以使用if語句也可以使用switch語句。如果遊樂場藍圖眾多,建議使用switch語句讓程式碼簡單一點,這裡使用switch語句。透過case判斷出正確的型態後使用「as!」向下轉型成正確型態並呼叫該型態專屬的方法。

func buildPlayground(_ playground: Playground) {
    playground.buildRollerCoaster()
    switch playground {
    case is MoreFun:
        (playground as! MoreFun).buildFerrisWheel()
    default:
        break
    }
}

buildPlayground(valley)
// Prints "森林谷建立了雲霄飛車"
buildPlayground(desert)
// Prints "乾燥沙漠建立了360度雲霄飛車"
// Prints "乾燥沙漠建立了摩天輪"

除了方法可以覆寫外,屬性也可以覆寫。但屬性覆寫只能覆寫計算型屬性,儲存型屬性無法覆寫。這是因為儲存型屬性只是儲存資料而已,沒有覆寫的必要,而計算型屬性可以透過覆寫改掉裡面的程式碼。除此之外,如果父類別的屬性是唯讀屬性,可以透過覆寫變成可讀可寫,但反過來不行。屬性的詳細說明請參考第9章屬性與方法。

舉個例子,在Playground定義了遊樂場的吉祥物為「熊讚」,這是一個唯讀屬性,因此所有根據Playground所建立出來的遊樂場實體,吉祥物都是熊讚並且不能更改。我們希望在MoreFun中可以修改吉祥物,因此,透過override讓吉祥物屬性變成可讀可寫,這樣一來,所有根據MoreFun產生的實體,或是其他繼承MoreFun的類別都有能力可以選擇自己的吉祥物了。

class Playground {
    var mascot: String? {
        "熊讚"
    }
}

class MoreFun: Playground {
    private var _mascot: String?
    override var mascot: String? {
        set {
            _mascot = newValue
        }
        get {
            _mascot
        }
    }
}

var desert = MoreFun()
desert.mascot = "黑麻糬"

~補充說明~
為了說明方便,上述程式碼移除了初始化器與其他建立遊樂設施的方法,若要執行,請參考更前面的例子將初始化器與方法加回去。

避免覆寫

有時候會希望屬性或方法不可以被覆寫,這時只要加上final修飾子即可。可以使用於final var、final func、final class func與final subscript,這些被加上final保留字的項目就是到此為止,不可以被覆寫了。例如在Playground的吉祥物屬性前加上final,這樣其他繼承Playground的遊樂場藍圖都不可以覆寫吉祥物屬性,也代表吉祥物一定是熊讚了。

class Playground {
    final var mascot: String? {
        "熊讚"
    }
}

如果希望整個類別都是final,也就是不可被繼承,只要將類別定義為final class即可,例如:

final class MoreFun: Playground {

}

這樣MoreFun就不可以再被其他類別當成父類別了。

8.4 初始化

結構與類別在建立實體的時候,所有的儲存類型的屬性都必須初始化,也就是要給一個值,nil也可以,Swift不允許實體化後儲存類型的屬性值是未初始化的。初始化除了在定義屬性時就給初始化值之外,必須將初始化程序放在初始化器裡面,最簡單的初始化器語法如下:

init() {
    // 初始化程式碼寫這
}

初始化器語法格式與函數寫法一樣,只是前面不用加上func保留字,然後也沒有傳回值,畢竟不是函數,所以這裡把他稱為初始化器(initializer)。當然也有很多人稱之為初始化函數,雖然嚴格講起來不是函數,但是大家也接受這樣的講法。初始化器中的程式碼會在實體產生時執行,執行完後實體就可以開始使用了。

結構的初始化器

以下範例定義了一個攝氏溫度的結構,這個結構中有一個溫度屬性,並且設計了初始化器,給用來儲存溫度的屬性一個初始值。

struct Celsius {
    var temperature: Double
    init() {
        temperature = 25.0
    }
}

有了初始化器之後在產生Celsius實體時,實體中的屬性temperature就已經初始化為25.0。

var temp = Celsius()
print(temp.temperature)
// Prints "25.0"

除了使用「Celsius()」來產生實體外,也可以使用呼叫初始化器的方式來產生實體,但因為這種寫法比較囉唆,所以很少看到這樣使用,但兩者的結果是完全一樣的。

var temp = Celsius.init()

結構允許定義屬性時不用先給初始化值並且也不用在初始化器中初始化屬性,但類別不行,對類別而言,宣告時沒有初始化的屬性必須實作一個初始化器,並且在初始化器中給屬性初始化值,這部分稍後再說明,我們先來看沒有初始化屬性的結構如何初始化值。下面這個例子說明了如果結構中有屬性但沒有初始化,在產生實體的時候,會要求加上與未初始化屬性相對映的參數,其參數標籤與屬性名稱一樣,透過這個方式來完成屬性初始化。這些參數是強制要求加入的,我們無法呼叫零參數的初始化器。

struct Celsius {
    var temperature: Double
}

var temp = Celsius(temperature: 25)

如果結構中的屬性在宣告時就已經初始化了,這樣我們可以不用在結構中實作初始化器,以下面這個結構為例,temperature已經初始化為25.0了。

struct Celsius {
    var temperature = 25.0
}

var temp = Celsius()

雖然我們在上面這個結構中沒有設計初始化器,但是我們還是可以呼叫初始化器,例如下面兩個實體化方式都是正確的,變數t1的temperature值是預設的25,而變數t2傳入了30,所以t2的temperature為30。

var t1 = Celsius()
var t2 = Celsius(temperature: 30)

當結構中沒有實作初始化器的時候,編譯器會自動根據屬性初始化的狀況幫我們補進去,補進去的初始化器類似下面這樣的形式,所以變數t1、t2的實體化方式才不會產生語法錯誤。

struct Celsius {
    var temperature = 25.0
    init() {}
    init(temperature: Double) {
        self.temperature = temperature
    }
}

不同類型的初始化器

除了init()這種零參數的初始化器外,還可以設計帶參數的初始化器,讓產生實體的時候能夠更彈性的初始化屬性值。這裡繼續以上個單元的Celsius結構為例,假設25度是攝氏溫度,如果我們希望初始化的時候還可以輸入華氏溫度,這時候我們可以設計不同的初始化器,讓呼叫者在呼叫時可以選擇。例如下面程式碼中的第一個初始化器不帶參數,然後設定temperature為25,這代表攝氏溫度;第二個初始化器很明顯是輸入華氏溫度,然後轉成攝氏溫度後存入temperature;第三個初始化器的參數不帶參數標籤,直接輸入一個攝氏溫度後存入temperature。

struct Celsius {
    var temperature: Double
    init() {
        temperature = 25
    }
    init(fahrenheit: Double) {
        temperature = (fahrenheit - 32.0 ) / 1.8
    }
    init(_ celsius: Double) {
        temperature = celsius
    }
}

上述程式碼中的三種不同格式的初始化器,讓該結構實體化時可以選擇不同的實體化方式,如下:

var t1 = Celsius()
// t1.temperature == 25
var t2 = Celsius(fahrenheit: 50)
// t2.temperature == 10
var t3 = Celsius(30)
// t3.temperature == 30

前面提過結構是value type,因此只要產生的實體為常數,即便結構中的屬性是var(代表可變動),常數實體也無法修改其屬性值。例如宣告temp是常數,初始化後要修改屬性值,就會得到錯誤訊息。

let temp = Celsius()
temp.temperature = 30
// Cannot assign to property: 'temp' is a 'let' constant

類別的初始化器

類別初始化程序與結構一樣,唯一差別在於類別中如果有屬性還沒有初始化時,語法檢查器就會強制要求實作初始化器,例如下面這個例子會得到一個沒有初始化器的錯誤訊息,原因是屬性temperature沒有初始化。

class Celsius {
    var temperature: Double
}
// Class 'Celsius' has no initializers

所以至少要實作一個初始化器,並且在裡面初始化temperature,加上這個初始化器以後,這個類別在實體化時就與結構的實體化方式一樣了。

class Celsius {
    var temperature: Double
    init() {
        temperature = 25.0
    }
}

當然我們也可以在宣告屬性的時候就同時初始化,這樣就不用實作初始化器,例如:

class Celsius {
    var temperature = 25.0
}

另一個類別與結構很大不同的地方是,在類別中即使產生的實體最後儲存到常數中,但因為類別屬於reference type,所以只要類別中的屬性使用var來宣告(代表可變動),則不論類別實體是儲存在變數還是常數中都允許修改屬性值,如下:

let temp = Celsius()
temp.temperature = 30
print(temp.temperature)
// Print "30.0"

要特別留意類別有這樣的特性,常數也可以修改實體中的屬性。

唯讀屬性初始化

唯讀屬性的初始化方式除了在宣告的同時就初始化外,也可以在初始化器中初始化,不論哪一種,一旦初始化後就無法再修改內容了。

struct Download {
    let url: String
}
let dl = Download(url: "https://demo.ip/")

如果是類別,必須自行實作初始化器,如下:

class Download {
    let url: String
    init(url: String) {
        self.url = url
    }
}
let dl = Download(url: "https://demo.ip/")

類別也可以這樣寫:

class Download {
    let url: String
    init() {
        url = "https://demo.ip/"
    }
}
let dl = Download()

類別當然也可以這樣寫:

class Download {
    let url = "https://demo.ip/"
}
let dl = Download()

Designated與Convenience初始化

再以本章第二節的遊樂場為例子,先把類別中建立摩天輪這些方法移除,我們只要關注初始化器就好。在下面這段程式碼中,設計了兩個初始化器,當產生實體並且沒有初始化名字時,預設的名字為 [Unnamed]。

class Playground {
    var name: String

    init(name: String) {
        self.name = name
    }
    
    init() {
        name = "[Unnamed]"
    }
}

現在有兩個初始化器都可以直接修改屬性name的內容,這兩個初始化器彼此獨立運作,誰也不依賴誰,所以這種初始化器稱為「Designated Initializer」。有些初始化器中的程式碼比較複雜,不像這個範例只有一行,個別獨立運作的初始化器極有可能會出現重複的程式碼,甚至出現邏輯規則不一致的現象,為了解決這個問題,我們讓初始化器彼此之間可以呼叫,如同函數呼叫一樣,這樣就可以有效減少重複的程式碼,如下所示。我們把所有命名的邏輯規則全部放到第一個初始化器,然後在第二個初始化器裡面呼叫第一個初始化器,呼叫時一定要加上self前綴詞,否則語法錯誤。這種必須藉由另外一個初始化器幫忙的初始化器,稱為「Convenience Initializer」,因此,最前面必須加上「convenience」修飾子。

class Playground {
    var name: String

    init(name: String?) {
        self.name = name ?? "[Unnamed]"
    }
    
    convenience init() {
        self.init(name: nil)
    }
}

~補充說明~
結構裡面的初始化器也可以呼叫另外一個初始化器,但是不可以加上「convenience」修飾子,這是因為結構沒有繼承功能,所以在結構中不需要去識別初始化器的類型。

初始化器彼此之間呼叫有三個規則必須遵守,透過這三個規則就可以確保所有有繼承關係的類別中的全部屬性一定都會被初始化,不會有漏掉的。

規則一: 子類別的Designated初始化器只能呼叫父類別的Designated初始化器。
規則二: Convenience初始化器只能呼叫同類別中的初始化器。
規則三:Convenience初始化器最後必須要呼叫Designated初始化器。

現在我們把類別MoreFun加進來,看看他與類別Playground間的初始化器怎麼合作來初始化所有屬性。我們在MoreFun中加上一個新的屬性ticketbooth,用來紀錄MoreFun遊樂場總共規劃幾個售票亭,這個屬性在Playground中沒有,因此MoreFun必須自己初始化這個屬性。

class MoreFun: Playground {
    var ticketbooth: Int
    convenience init() {
        self.init(name: nil)
    }
    
    convenience init(ticketbooth: Int) {
        self.init(name: nil, ticketbooth: ticketbooth)
    }
    
    convenience override init(name: String?) {
        self.init(name: name, ticketbooth: 1)
    }
    
    init(name: String?, ticketbooth: Int) {
        self.ticketbooth = ticketbooth
        super.init(name: name)
    }
}

用圖示顯示Playground與MoreFun的初始化程序會更清楚,如下:

從圖上來看初始化程序的三個規則,簡單來說就是Designated初始化器從子類別一路往上呼叫父類別的Designated初始化器,直到最頂層為止;各類別中的Convenience初始化器最後一定要呼叫到同類別中的Designated初始化器。

我們將MoreFun類別實體化,看看各種實體化後的屬性值為何。

let fun1 = MoreFun()
// name == "[Unnamed]", ticketbooth == 1
let fun2 = MoreFun(name: "乾燥沙漠")
// name == "乾燥沙漠", ticketbooth == 1
let fun3 = MoreFun(ticketbooth: 4)
// name == "[Unnamed]", ticketbooth == 4
let fun4 = MoreFun(name: "黑森林", ticketbooth: 3)
// name == "黑森林", ticketbooth == 3

兩階段初始化

由於類別的初始化程序比較複雜,為了避免在執行某些方法或存取屬性時尚未初始化完成導致程式當掉,Swift使用了兩階段初始化程序來確保呼叫方法或存取屬性前,初始化已全部完成。

階段一:
開始呼叫類別的初始化器並確認該類別中所有的屬性都完成初始化。若該類別為子類別時,必須等到子類別中所有屬性都初始完成後才能呼叫父類別的初始化器。重複這個程序,直到最頂層類別初始化完成。當最頂層類別初始化完成代表階段一結束。

階段二
從最頂層類別開始存取屬性或呼叫方法,完成後交由子類別開始存取屬性或呼叫方法。重複這個程序,直到底層最初需實體化的類別為止。當最初需實體化的類別實體化後,第二階段結束。

用下面這個例子來說明兩階段初始化。

class Superclass {
    var name: String
    init(name: String) {
        self.name = name
        identityNumber()
    }
    func identityNumber() -> String {
        name
    }
}

class Subclass: Superclass {
    var id: Int
    init(name: String, id: Int) {
        self.id = id
        super.init(name: name)
    }
    override func identityNumber() -> String {
        name + String(id)
    }
}

當實體化Subclass時,如下程式碼:

let instance = Subclass(name: "David", id: 1100)
print(instance.identityNumber())
// Prints "David1100"

階段一:
Subclass的初始化器init(name:id:)會被呼叫,根據階段一規定,自己的屬性必須先初始化完成才可以呼叫父類別的初始化器,因此「self.id = id」必須先執行完才可以呼叫super.init(name:),否則違反階段一規定,編譯器會發出錯誤訊息。當Subclass完成所有的屬性初始化後,開始呼叫super.init(name:),此時Superclass的初始化器init(name:)先執行「self.name = name」,當這行執行完後Superclass中所有的屬性都初始化完成,因此階段一結束。

段二:
Superclass初始化器init(name:)中的identityNumber()開始執行,執行完後(因為多型特性,此時呼叫的是Subclass的identityNumber(),因此印出David1100)回到Subclass的初始化器init(name:id:),檢查super.init(name: name)之後是否還有程式碼需要執行,如果沒有,階段二結束。

隱性與顯性呼叫

按照兩階段初始化的規定,子類別一定需要呼叫父類別的Designated初始化器,但我們看下面這個例子。Subclass中的兩個初始化器裡面都沒有呼叫super.init(),而編譯器的語法檢查沒有發出錯誤。但Subclass實體化後屬性color的內容為”white”,表示Superclass的初始化器有正確執行,這代表super.init()被省略了。在特定情況下super.init()確實可以省略,此時稱為隱性呼叫。如果子類別的初始化器已經完成了所有需要初始化的屬性,然後又沒有修改屬性值或呼叫其他的方法,並且父類別中有零參數的init()初始化器,這時候呼叫super.init()可以省略。

class Superclass {
    var color: String
    init() {
        color = "white"
    }
}

class Subclass: Superclass {
    var number: Int
    override init() {
        number = 0
    }
    init(number: Int) {
        self.number = number
    }
}

var instance = Subclass()
print(instance.color)
// Prints "white"

前面說明了隱性呼叫,接下來這種情況非要使用顯性呼叫不可,程式碼如下。因為在number = 0之後要去修改屬性color的內容,這已經屬於第二階段初始化了,所以必須等第一階段初始化完成才能進入第二階段,這時候super.init()不可以省略,因為編譯器不知道該在何時幫我們插入super.init(),所以我們必須明確的告訴編譯器呼叫時機,此為顯性呼叫。

class Subclass: Superclass {
    var number: Int
    override init() {
        number = 0
        super.init()
        color = "black"
    }
}

順道一提,根據初始化器呼叫的三個規則,子類別使用隱性呼叫時父類別的零參數init()不可以是Convenience初始化器。

初始化器的繼承

在預設情況下子類別並不繼承父類別的初始化器,但有兩種情況例外:

情況一: 如果子類別沒有定義任何初始化器,則子類別繼承父類別全部的初始化器。

情況二:如果子類別覆寫父類別全部的Designated初始化器,則子類別自動繼承父類別全部的Convenience初始化器。

根據以上兩個情況可以推論出,只要子類別實作了初始化器,或者子類別覆寫父類別中部分的Designated初始化器,這時子類別就不會繼承父類別的初始化器了。

必須實作

由初始化器的繼承情況可知,有的時候父類別的初始化器可能到子類別就不見了,這時可以在父類別的初始化器前加上required修飾子,這樣子類別就一定要實作這個初始化器,並且再往下繼承者也需要。例如父類別的初始化器加上了required子類別就一定要實作,並且在子類別中也要加上required,用來確保再之後的子類別也需要實作,如下:

class Superclass {
    var color: String
    required init(color: String) {
        self.color = color
    }
}

class Subclass: Superclass {
    required init(color: String) {
        super.init(color: color)
    }
}

如果子類別沒有實作任何初始化器,即使父類別中有required初始化器,語法上也不會產生錯誤。原因是根據初始化繼承的情況一所描述的:「如果子類別沒有實作任何一個初始化器,就自動繼承父類別的所有初始化器」,這時當然包含了父類別的required初始化器,因此,在這種情況下,子類別沒有實作required初始化器是正確的語法。

初始化失敗

結構或類別的初始化程序不一定會成功,如果失敗,我們可以透過回傳nil的方式告訴呼叫者初始化失敗了,也代表實體沒有建立起來。初始化器如果要回傳nil(也只能回傳nil),只要在init後方加上問號即可,舉例如下:

struct AlwaysPositive {
    let value: UInt
    init?(value: Int) {
        guard value >= 0 else {
            return nil
        }
        self.value = UInt(value)
    }
}

if let instance = AlwaysPositive(value: -5) {
    print(instance.value)
} else {
    print("實體建立失敗")
}
// Prints "實體建立失敗"

這個初始化器用來確保初始值一定要輸入正值,如果輸入負值會導致初始化失敗(例如上面程式碼中的-5),只要初始化失敗,實體就不會建立起來,所以使用if let語句來檢查是否產生nil。

8.5 反初始化

反初始化器(deinitializer)會在實體所佔用的記憶體區塊被回收前最後一刻呼叫,因此可以在這個時機作一些事情,例如將未存檔的資料存檔,呼叫遠端系統進入省電模式…等。反初始化只能作用於類別,並且每個類別只能有一個反初始化器,而且不可以有參數,語法如下:

deinit {
    // 程式碼寫這
}

測試反初始化器是否運作的方式是將實體設定為nil,這時表示不會再有人需要使用這個實體,此時作業系統開始進行記憶體回收程序,並且呼叫反初始化器,例如:

class Playground {
    var name: String
    
    init?(name: String) {
        self.name = name
    }

    func buildRollerCoaster () {
        print("\(name)建立了雲霄飛車")
    }

    deinit {
        print("\(name)即將被拆除")
    }
}

var space = Playground(name: "太空城")
space?.buildRollerCoaster()
space = nil
// Prints "太空城建立了雲霄飛車"
// Prints "太空城即將被拆除"

8.6 擴充

擴充是用來增加結構、類別、列舉以及協定這四種原有功能上的不足。擴充之後,所有屬於這些型態的變數或常數就可以使用擴充功能了。擴充的語法只要在想要擴充的型態前面加上extension即可,如下:

extension SomeType {
    // 擴充的屬性或方法寫這
}

也可以透過extension讓某個原本沒有符合協定的型態開始符合某個協定的規範,如下:

extension SomeType, SomeProtocol, AnotherProtocol {
    // 擴充屬性與方法寫這
}

舉個例子。如果我們想要知道數字29是不是質數,通常會寫一個函數,函數中計算傳進來的參數值是否為質數,例如傳入29,該函數計算完後傳回true代表是質數,傳回false代表不是質數。除了這樣做之外,我們也可以選擇擴充Int型態,讓任何屬於Int型態的變數或常數都自帶一個判斷是否為質數的功能。如下:

extension Int {
    var isPrime: Bool {
        guard self > 1 else {
            return false
        }
        
        for i in 2..<self {
            if self % i == 0 {
                return false
            }
        }
        return true
    }
}
print(23.isPrime)
// Print "true"
print(36.isPrime)
// Print "false"

~補充說明~
質數的定義為大於1的自然數中,除了1與本身外,無法被其他自然數整除。

使用擴充的概念是,既然這個功能是這個資料型態專有的,那就應該放在這個型態中讓屬於這個型態的所有變數或常數都可以使用,也是一種物件導向封裝的概念。

繼續擴充

擴充強大的地方在於擴充完後可以再擴充下去而不需要去修改第一次擴充的原始碼,例如我們想要列出到給定數字間的所有質數。之前我們已經透過擴充功能讓Int型態可以自己判斷是否為質數,現在我們再擴充一次,讓列出範圍內所有質數的這個新函數可以呼叫之前已經擴充的功能。如下:

extension Int {
    func primes() -> [Int] {
        var numbers = [Int]()
        for i in 2...self {
            if i.isPrime {
                numbers.append(i)
            }
        }
        return numbers
    }
}
print(100.primes())
// Prints [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]

再看另外一個例子。下面這個範例是將數字轉成大寫國字,在很多生活應用上會需要這個功能,例如開支票,開收據或是郵局匯款,都需要填大寫國字。這裡的範例比較簡單,僅僅將數字對映成大寫國字,並沒有加上仟、佰、拾等單位用詞。

extension Int {
    func toChinese() -> String {
        let words = Array("零壹貳叁肆伍陸柒捌玖")
        return  String(self)
                 .map { String(words[Int(String($0))!]) }
                 .joined()
    }
}

print(203.toChinese())
// Prints = "貳零叁"
print(1005.toChinese())
// Prints = "壹零零伍"

~補充說明~
return後方連續使用好幾個型別轉換,主要目的是根據數字例如203轉成字串”203″後使用map(_:)函數將每一個字元轉成陣列索引值後與words陣列對映,對映結果是大寫國字組成的字元陣列,然後將此字元陣列轉成字串陣列後再用joined()合併成單一字串傳回。

對類別來說,擴充與繼承都是用來增加原本類別缺少的功能,但擴充與繼承不一樣的地方是,擴充的結果會實際改掉原本類別有的功能,但繼承有點類似複製貼上,將原有的功能複製起來貼上時再加上新功能,所以原有的還在,然後多了一份新的。

8.7 自訂運算子

所謂的運算子就是「+」「-」「*」「/」「>」「<」「!」…等這些,負責對資料進行一些特別運算。同一個運算子在不同的資料型態中會有不同的效果,例如Int的「+」是將兩個數字加給來,但是String的「+」是將兩個字串合併起來。在Swift標準函數庫中的各種資料型態,多多少少都會有一些運算子,讓我們可以用很簡潔的語法來操作資料。

除了已經有的運算子外,Swift允許我們可以自己定義運算子,不論是在我們自己設計的結構或類別中,還是在extension中都可以自訂運算子。運算子有三種類型,如下:

infix(預設值):     此運算子放在兩個運算元中間,例如 2 + 3。
prefix:   此運算子放在運算元前面,例如not運算子「!」,布林運算中常見的 !a就是了。
postfix:  此運算子放在運算元後面,例如 a? 或 a!。

舉個例子。我們想要計算數字m的n次方,我們知道有pow(_:_:)函數可以使用,但我們希望可以透過「**」運算子,讓次方計算的程式碼更簡潔,例如 5 ** 3 就代表要執行5的3次方運算。我們只要一開始告訴編譯器「**」是一個infix類型的運算子,然後透過extension將「**」運算子加到Decimal型態中就可以了。從程式碼可以發現,其實運算子也相當於是一個函數,只是語法規定在前面要加上static修飾子。

infix operator **
extension Decimal {
    static func ** (lhs: Decimal, rhs: Int) -> Decimal {
        pow(lhs, rhs)
    }
}

現在我們可以使用 ** 運算子來計算m 的 n次方了。

print(5 ** 3)
// Prints "125"
print(2.3 ** 4)
// Prints "27.9841"

再舉個prefix例子。我們想要透過運算子「^」來計算數字的平方根,如下:

prefix operator ^
extension Double {
    static prefix func ^ (n: Double) -> Double {
        n.squareRoot()
    }
}

print(^2)
// Prints "1.4142135623730951"
print(^9)
// Prints "3.0"

並不是每一個符號都可以拿來做運算子,可以拿來做運算子的符號為:

/、 =、- 、+、!、*、%、<、>、&、|、^、~與?。此外還有一些Unicode編碼才能產生的符號,這裡就不多做介紹了,我們就使用鍵盤打的出來的符號當運算子應該就足夠使用。

8.8 Optional Chaining

Optional chaining的意思是透過連續幾個Optional型態的屬性或方法存取最終屬性或方法的技術。由於鏈結過程中有很多地方都可能會出現nil,但optional chaining讓我們不用一個一個去檢查每一層的屬性或方法否為nil(這太麻煩了),我們只要對整個鏈結結果去檢查是否為nil即可。

先來看一個簡單的例子。每個員工都有所屬辦公室,每間辦公室有辦公室編號,設計如下:

struct Office {
    var id: String
}

struct Staff {
    var office: Office?
}

當有一個新員工應聘到公司時,可能某些原因暫時還沒有分配到辦公室,因此想要知道員工的辦公室編號時,一定要先確認該員工是否有辦公室。在Staff結構中加上一個印出辦公室編號的方法officeId(),在這方法中先用if let判斷屬性office是否為nil,如果不是nil代表已經有辦公室,如果為nil代表還沒有辦公室。以下面這個例子,員工staff01還沒有辦公室。

struct Staff {
    var office: Office?
    
    func officeId() {
        if let office = office {
            print(office.id)
        } else {
            print("還沒有辦公室")
        }
    }
}

var staff01 = Staff()
staff01.officeId()
// Prints "還沒有辦公室"

要將員工歸屬到某間辦公室,只要先取得該辦公室的實體,然後將該實體指定給Staff的office屬性即可。

var office = Office(id: "行動應用與尖端發展")
var staff01 = Staff()
staff01.office = office
staff01.officeId()
// Prints "行動應用與尖端發展"

如果員工原本有辦公室但被暫時取消了,只要將屬性office改為nil,這位員工就沒有辦公室了。

staff01.office = nil
staff01.officeId()
// Prints "還沒有辦公室"

辦公室一定有正在進行的計畫,所以加上一個Project結構用來管理公司的各個計畫,並且在Office中儲存哪些計畫是該辦公室負責的。由於考量一個新成立的辦公室可能還沒有計畫,因此屬性missions的型態為Optional型態。

struct Project {
    var id: Int
}

struct Office {
    var id: String
    var missions: [Project]?
}

現在我們在Staff結構中加上一個方法,用來列出員工目前正在進行的任務,若要先檢查屬性office是否為nil,然後再檢查屬性missions是否為nil就太麻煩了,我們來看optional chaining怎麼運作的。

struct Staff {
    var office: Office?
    
    func officeId() {
	...
    }
    
    func missionsList () {
        guard let missions = office?.missions else {
            print("沒有任何任務正在進行")
            return
        }
        
        missions.forEach {
            print($0)
        }
    }
}

在optional chaining的過程中,屬性office與missions都有可能為nil,但我們不需要一層一層去檢查,因為optional chaining只要遇到nil時就會立刻終止鏈結並且以nil返回。所以當office為nil時,office?.missions就會立刻斷開,不用擔心系統會用nil再去鏈結missions屬性導致程式當掉。如果不用guard let可以更清楚看到optional chaining的運作。將missionsList()方法改為下面這樣,只要office或missions任何一個屬性為nil,optional chaining就會立刻斷開鏈結,最後的forEach不會執行。

func missionsList () {
    office?.missions?.forEach {
        print($0)
    }
}

如果員工有辦公室,辦公室也有任務正在進行中,missionsList()方法就會順利印出專案內容。

let projects = [Project(id: 1), Project(id: 2), Project(id: 3)]
var office = Office(id: "行動應用與尖端發展")
var staff01 = Staff()

office.missions = [projects[0], projects[2]]
staff01.office = office
staff01.missionsList()
// Prints "Project(id: 1)"
// Prints "Project(id: 3)"

上面這段程式碼各屬性值的指派方式是先將計畫放到office的missions屬性中,然後再將office放到staff01的office屬性中。我們也可以使用optional chaining的方式來指派屬性值,例如:

staff01.office = office
staff01.office?.missions = [projects[0], projects [2]]

上面這段程式碼optional chaining會自動檢查office?是否為nil,如果不是就會繼續鏈結missions,如果office?為nil,這時鏈結就會斷開,導致計畫無法存入missions屬性。換句話說,這兩行執行順序如果對調,屬性missions的內容會是nil。

Optional chaining可以深入到陣列中的每一個元素也就是可以到下標語法,例如我們想要知道員工任務列表中的第一個計畫,可以這樣寫:

if let firstMission = staff01.office?.missions?[0] {
    print("第一個計畫為 \(firstMission)")
}
// Prints "第一個計畫為 Project(id: 1)"

發表迴響