讀古今文學網 > iOS編程基礎:Swift、Xcode和Cocoa入門指南 > 4.4 類 >

4.4 類

類與結構體相似,但存在如下一些主要差別:

引用類型

類是引用類型。這意味著類實例有兩個結構體或枚舉實例所不具備的關鍵特性。

可變性

類實例是可變的。雖然對類實例的引用是個常量(let),不過你可以通過該引用修改實例屬性的值。類的實例方法絕不能標記為mutating。

多引用

如果給定的類實例被賦予多個變量或作為參數傳遞給函數,那麼你就擁有了對相同對象的多個引用。

繼承

類可以擁有子類。如果一個類有父類,那麼它就是這個父類的子類。這樣,類就可以構成一種樹形結構了。

在Objective-C中,類是唯一一種對像類型。一些內建的Swift結構體類型會橋接到Objective-C的類類型,不過自定義的結構體類型卻做不到這一點。因此,在使用Swift進行iOS編程時,使用類而非結構體的一個主要原因就是它能夠與Objective-C和Cocoa互換。

4.4.1 值類型與引用類型

枚舉與結構體是一類,類是另一類,這兩類之間的主要差別在於前者是值類型,而後者是引用類型。

值類型是不可變的。實際上,這意味著你無法修改值類型實例屬性的值。看起來可以修改,但實際上是不行的。比如,我們考慮一個結構體。結構體是值類型:


struct Digit {
    var number : Int
    init(_ n:Int) {
        self.number = n
    }
}
  

看起來好像可以修改Digit的number屬性。畢竟,這是將該屬性聲明為var的唯一目的;Swift的賦值語法使我們相信修改Digit的number是可行的:


var d = Digit(123)
d.number = 42
  

但實際上,在上述代碼中,我們並未修改這個Digit實例的number屬性;我們實際上創建了一個不同的Digit實例並替換掉了之前那個。要想證明這一點,我們添加一個Setter觀察者:


var d : Digit = Digit(123) {
    didSet {
        print("d was set")
    }
}
d.number = 42 // "d was set"
  

一般來說,當修改一個實例值類型時,你實際上會通過另一個實例替換掉當前這個實例。這說明了如果對該實例的引用是通過let聲明的,那麼這就是無法修改值類型實例的原因。如你所知,使用let聲明的初始化變量是不能被賦值的。如果該變量指向了值類型實例,並且該值類型實例有一個屬性,即便這個屬性是通過var聲明的,如果我們對該屬性賦值,那麼編譯器就會報錯:


struct Digit {
    var number : Int
    init(_ n:Int) {
        self.number = n
    }
}
let d = Digit(123)
d.number = 42 // compile error
  

原因在於這種修改需要替換掉d盒子中的Digit實例。不過,我們無法通過另一個Digit實例替換掉d所指向的Digit實例,因為這意味著要對d賦值,而let聲明是不允許這麼做的。

反過來,這正是設置實例屬性的結構體或枚舉的實例方法要被顯式標記為mutating關鍵字的原因所在。比如:


struct Digit {
    var number : Int
    init(_ n:Int) {
        self.number = n
    }
    mutating func changeNumberTo(n:Int) {
        self.number = n
    }
}
  

如果不使用mutating關鍵字,那麼上述代碼將無法編譯通過。mutating關鍵字會讓編譯器相信你知道這裡會產生什麼樣的結果:如果方法被調用了,那麼它會替換掉這個實例,這樣該方法只能在使用var聲明的引用上進行調用,let則不行:


let d = Digit(123)
d.changeNumberTo(42) // compile error
  

不過,我所說的這一切都不適用於類實例!類實例是引用類型,而非值類型。如果一個類的實例屬性可以被修改,那麼顯然要用var聲明;不過,若想通過類實例的引用來設置屬性,那麼引用是無須聲明為var的:


class Dog {
    var name : String = "Fido"
}
let rover = Dog
rover.name = "Rover" // fine
  

在上面最後一行代碼中,rover所指向的類實例會在原地被修改。這裡不會對rover進行隱式賦值,因此let聲明是無法阻止修改的。在設置屬性時,Dog變量上的Setter觀察者是不會被調用的:


var rover : Dog = Dog {
    didSet {
        print("did set rover")
    }
}
rover.name = "Rover" // nothing in console
  

如果顯式設置rover(設為另一個Dog實例),那麼Setter觀察者會被調用;不過,這裡僅僅是修改了rover所指向的Dog實例的一個屬性,因此Setter觀察者不會被調用。

這些示例都涉及聲明的變量引用。對於函數調用的參數來說,值類型與引用類型之間的差別依然存在,並且與之前所述一致。如果嘗試對枚舉參數的實例屬性或結構體參數的實例屬性賦值,那麼編譯器就會報錯。如下代碼無法編譯通過:


func digitChanger(d:Digit) {
    d.number = 42 // compile error
}
  

要想讓上述代碼編譯通過,請使用var來聲明參數:


func digitChanger(var d:Digit) {
    d.number = 42
}
  

但如下函數聲明沒有使用var依然也能編譯通過:


func dogChanger(d:Dog) {
    d.name = "Rover"
}
  

值類型與引用類型存在這些差別的深層次原因在於:對於引用類型來說,在對實例的引用與實例本身之間實際上存在一個隱藏的間接層次;引用實際上引用的是對實例的指針。這又引申出了另一個重要的隱喻:在將類實例賦給變量或作為參數傳遞給函數時,你可以使用針對同一個對象的多個引用。但結構體與枚舉卻不是這樣。在將枚舉實例或結構體實例賦給變量、傳遞給函數,或從函數返回時,真正賦值或傳遞的本質上是該實例的一個新副本。不過,在將類實例賦給變量、傳遞給函數,或從函數返回時,賦值或傳遞的是對相同實例的引用。

為了證明這一點,我將一個引用賦給另一個引用,然後修改第2個引用,接下來看看第1個引用會發生什麼。先來看看結構體:


var d = Digit(123)
print(d.number) // 123
var d2 = d // assignment!
d2.number = 42
print(d.number) // 123
  

上述代碼修改了結構體實例d2的number屬性;這並不會影響d的number屬性。下面再來看看類:


var fido = Dog
print(fido.name) // Fido
var rover = fido // assignment!
rover.name = "Rover"
print(fido.name) // Rover
  

上述代碼修改了類實例rover的name屬性,fido的name屬性也隨之發生了變化!這是因為第3行的賦值語句執行後,fido與rover都指向了相同的實例。在對枚舉或結構體實例賦值時,實際上會執行複製,創建出全新的實例。不過在對類實例進行賦值時,得到的是對相同實例的新引用。

參數傳遞亦如此。先來看看結構體:


func digitChanger(var d:Digit) {
    d.number = 42
}
var d = Digit(123)
print(d.number) // 123
digitChanger(d)
print(d.number) // 123
  

我們將Digit結構體實例d傳遞給了函數digitChanger,它會將局部參數d的number屬性設為42。不過,Digit實例d的number屬性依然為123。這是因為,傳遞給digitChanger的Digit是個完全不同的Digit。作為函數實參傳遞Digit的動作會創建一個全新的副本。不過對於類實例來說,傳遞的是對相同實例的引用:


func dogChanger(d:Dog) { // no "var" needed
    d.name = "Rover"
}
var fido = Dog
print(fido.name) // "Fido"
dogChanger(fido)
print(fido.name) // "Rover"
  

函數dogChanger中對d的修改會影響Dog實例fido!將類實例傳遞給函數並不會複製該實例,而更像是將該實例借給函數一樣。

可以生成相同實例的多個引用的能力在基於對像編程的世界中是非常重要的,其中對象可以持久化,並且其中的屬性也會隨之持久化。如果對像A與對像B都是長久存在的對象,並且它們都擁有一個Dog屬性(Dog是個類),將對相同Dog實例的引用分別傳遞給這兩個對象,對像A與對像B都可以修改其Dog屬性,那麼一個對像對Dog屬性的修改就會影響另一個對象。你持有著一個對象,然後發現它已經被其他人修改了。這個問題在多線程應用中變得更為嚴重,相同的對象可能會被兩個不同的線程修改;值類型就不存在這些問題;實際上,正是由於這個差別的存在,在設計對像類型時,你會更傾向於使用結構體而非類。

引用類型有缺點,但同樣也有優點!優點在於傳遞類實例變得非常簡單,你所傳遞的只是一個指針而已。無論對像實例有多大,多複雜;無論包含了多少屬性,擁有多少數據量,傳遞實例都是非常快速且高效的,因為整個過程中不會產生新數據。此外,在傳遞時,類實例更為長久的生命週期對於其功能性和完整性是至關重要的;UIViewController需要是類而不能是結構體,因為無論如何傳遞,每個UIViewController實例都會表示運行著的應用的視圖控制器體系中同一個真實存在且持久的視圖控制器。

遞歸引用

值類型與引用類型的另一個主要差別在於值類型從結構上來說是不能遞歸的:值類型的實例屬性不能是相同類型的實例。如下代碼無法編譯通過:


struct Dog { // compile error
    var puppy : Dog?
}
  

如Dog包含了Puppy屬性,同時Puppy又包含了Dog屬性等更為複雜的循環鏈也是不合法的。不過,如果Dog是類而不是結構體,那就沒問題了。這是值類型與引用類型在內存管理上的不同導致的(第5章將會詳細介紹引用類型內存管理,第12章會介紹這個話題)。

在Swift 2.0中,枚舉case的關聯值可以是該枚舉的實例,前提是該case(或整個枚舉)被標記為indirect:


enum Node {
    case None(Int)
    indirect case Left(Int, Node)
    indirect case Right(Int, Node)
    indirect case Both(Int, Node, Node)
}
  

4.4.2 子類與父類

兩個類彼此間可以形成子類與父類的關係。比如,我們有個名為Quadruped的類和名為Dog的類,並讓Quadruped成為Dog的父類。一個類可以有多個子類,但一個類只能有一個直接父類。這裡的「直接」指的是父類本身也可能有父類,這樣會形成一個鏈條,直到到達最終的父類,我們稱為基類或根類。由於一個類可以有多個子類,並且只有一個父類,因此會形成一個子類層次樹,每個子類都從其父類分支出來,同時頂部只有一個基類。

對於Swift語言本身來說,並不要求一個類必須要有父類;如果有父類,那麼最終也是從某個特定的基類延伸出來的。因此,Swift程序中可能會有很多類沒有父類,會有很多獨立的層次化子類樹,每棵樹都從不同的基類延伸出來。

不過,Cocoa卻不是這樣的。在Cocoa中只有一個基類:NSObject,它提供了一個類需要的所有必備功能,其他所有類都是該基類不同層次上的子類。因此,Cocoa包含了一個巨大的類層次樹,甚至在你編寫代碼或創建自定義類之前就是這樣的。我們可以將這棵樹畫出來作為一個大綱。事實上,Xcode可以呈現出這個大綱(如圖4-1所示):在iOS項目窗口中,選擇View→Navigators→Show Symbol Navigator並單擊Hierarchical,選中過濾欄上的第1個與第3個圖標(標記為藍色)。Cocoa類是NSObject下面的樹形結構的一部分。

圖4-1:Xcode中呈現的Cocoa類層次關係的一部分

起初,設定父類與子類關係的目的在於可以讓相關類共享一些功能。比如,我們有一個Dog類和一個Cat類,考慮為這兩個類聲明一個walk方法。因為狗與貓都是四肢動物,因此可以想像它們走路的方式大體上是相似的。這樣,將walk作為Quadruped類的方法會更合理一些,並且讓Dog與Cat成為Quadruped的子類。結果就是雖然Dog與Cat沒有定義walk方法,但卻可以向它們發送walk消息,這是因為它們都有一個擁有walk方法的父類。我們可以說子類繼承了父類的方法。

要想將某個類聲明為另一個類的子類,請在類聲明的類名後面加上一個冒號和父類的名字,比如:


class Quadruped {
    func walk  {
        print("walk walk walk")
    }
}
class Dog : Quadruped {}
class Cat : Quadruped {}
  

現在來證明Dog實際上繼承了Quadruped的walk方法:


let fido = Dog
fido.walk // walk walk walk
  

注意,在上述代碼中,可以向Dog實例發送walk消息,就好像walk實例方法是在Dog類中聲明的一樣,雖然實際上是在Dog的父類中聲明的,這正是繼承所起的作用。

子類化的目的不僅在於讓一個類可以繼承另一個類的方法;子類還可以聲明自己的方法。通常,子類會包含繼承自父類的方法,但遠非這些。如果Dog沒有定義自己的方法,那麼我們就很難看到它存在於Quadruped之外的原因。不過,如果Dog知道一些Quadruped所不知道的事情(如bark),那麼將其作為單獨一個類才有意義。如果在Dog類中聲明了bark方法,在Quadruped類中聲明了walk方法,並且讓Dog成為Quadruped的子類,那麼Dog就繼承了Quadruped類的行走能力,而且還可以bark:


class Quadruped {
    func walk  {
        println("walk walk walk")
    }
}
class Dog : Quadruped {
    func bark  {
        println("woof")
    }
}
  

下面證明一下:


let fido = Dog
fido.walk // walk walk walk
fido.bark // woof
  

一個類是否有一個實例方法並不是什麼重要的事情,因為方法可以聲明在該類中,也可以聲明在父類中並繼承下來。發送給self的消息在這兩種情況下都可以正常運作。如下代碼聲明了一個barkAndWalk實例方法,它向self發送了兩條消息,並沒有考慮相應的方法是在哪裡聲明的(一個在當前類中聲明的,另一個則繼承自父類):


class Quadruped {
    func walk  {
        print("walk walk walk")
    }
}
class Dog : Quadruped {
    func bark  {
        print("woof")
    }
    func barkAndWalk {
        self.bark
        self.walk
    }
}
  

下面證明一下:


let fido = Dog
fido.barkAndWalk // woof walk walk walk
  

子類還可以重新定義從父類繼承下來的方法。比如,也許一些狗的bark不同於別的狗。我們可以定義一個類NoisyDog,它是Dog的子類。Dog聲明了bark方法,不過NoisyDog也聲明了bark方法,並且其定義不同於Dog對其的定義,這叫作重寫。本質原則在於,如果子類重寫了從父類繼承下來的方法,那麼在向該子類實例發送消息時,被調用的方法是子類所聲明的那一個。

在Swift中,當重寫從父類繼承下來的東西時,你需要在聲明前顯式使用關鍵字override。比如:


class Quadruped {
    func walk  {
        print("walk walk walk")
    }
}
class Dog : Quadruped {
    func bark  {
        print("woof")
    }
}
class NoisyDog : Dog {
    override func bark  {
        print("woof woof woof")
    }
}
  

下面試一下:


let fido = Dog
fido.bark // woof
let rover = NoisyDog
rover.bark // woof woof woof
  

值得注意的是,子類函數與父類函數同名並不一定就是重寫。回憶一下,只要簽名不同,Swift就可以區分開同名的兩個函數,它們是不同的函數,因此子類中的實現並不是對父類中實現的重寫。只有當子類重新定義了繼承自父類的相同函數才是重寫,所謂相同函數指的是名字相同(包括外部參數名相同)和簽名相同。

很多時候,我們想要在子類中重寫某個東西,同時又想訪問父類中被重寫的對應物。這可以通過向關鍵字super發送消息來達成所願。NoisyDog中的bark實現就是個很好的示例。NoisyDog的吠叫與Dog基本上是一樣的,只不過次數不同而已。我們想要在NoisyDog的bark實現中表示出這種關係。為了做到這一點,我們讓NoisyDog的bark實現發送bark消息,但不是發送給self(這會導致循環),而是發送給super;這樣就會在父類而不是自己的類中搜索bark實例方法實現:


class Dog : Quadruped {
    func bark  {
        print("woof")
    }
}
class NoisyDog : Dog {
    override func bark  {
        for _ in 1...3 {
            super.bark
        }
    }
}
  

下面是調用:


let fido = Dog
fido.bark // woof
let rover = NoisyDog
rover.bark // woof woof woof
  

下標函數是個方法。如果父類聲明了下標,那麼子類可以通過相同的簽名聲明下標,只要使用關鍵字override指定即可。為了調用父類的下標實現,子類可以在關鍵字super後使用方括號(如super[3])。

除了方法,子類還可以繼承父類的屬性。當然,子類還可以聲明自己的附加屬性,可以重寫繼承下來的屬性(稍後將會介紹一些限制)。

可以在類聲明前加上關鍵字final防止類被繼承,也可以在類成員聲明前加上關鍵字final防止它被子類重寫。

4.4.3 類初始化器

類實例的初始化要比結構體或枚舉實例的初始化複雜得多,這是因為類存在繼承。初始化器的主要工作是確保所有屬性都有初值,這樣當實例創建出來後其格式就是良好的;初始化器還可以做一些對於實例的初始狀態與完整性來說是必不可少的工作。不過,類可能會有父類,也有可能擁有自己的屬性與初始化器。這樣,除了初始化子類自身的屬性並執行初始化器任務,我們必須要確保在初始化子類時,父類的屬性也被初始化了,並且初始化器會按照良好的順序執行。

Swift以一種一致、可靠且巧妙的方式解決了這個問題,它強制施加了一些清晰且定義良好的規則,用於指導類初始化器要做的事情。

1.類初始化器分類

這些規則首先對類可以擁有的初始化器種類進行了區分:

隱式初始化器

類沒有存儲屬性,或是存儲屬性都作為聲明的一部分進行初始化,沒有顯式初始化器,有一個隱式初始化器init()。

指定初始化器

在默認情況下,類初始化器是個指定初始化器。如果類中有存儲屬性沒有在聲明中完成初始化,那麼這個類至少要有一個指定初始化器,當類被實例化時,一定會有一個指定初始化器被調用,並且要確保所有存儲屬性都被初始化。指定初始化器不可以委託給相同類的其他初始化器;指定初始化器不能使用self.init(...)。

便捷初始化器

便捷初始化器使用關鍵字convenience標記。它是個委託初始化器,必須調用self.init(...)。此外,便捷初始化器必須要調用相同類的一個指定初始化器,否則就必須調用相同類的另一個便捷初始化器,這就構成了一個便捷初始化器鏈,並且最後要調用相同類的一個指定初始化器。

如下是一些示例。類沒有存儲屬性,因此它具有一個隱式init()初始化器:


class Dog {
}
let d = Dog
  

下面這個類的存儲屬性有默認值,因此它也有一個隱式init()初始化器:


class Dog {
    var name = "Fido"
}
let d = Dog
 

下面這個類的存儲屬性沒有默認值,它有一個指定初始化器,所有這些屬性都是在該指定初始化器中初始化的:


class Dog {
    var name : String
    var license : Int
    init(name:String, license:Int) {
        self.name = name
        self.license = license
    }
}
let d = Dog(name:"Rover", license:42)
  

下面這個類與上面的類似,不過它還有兩個便捷初始化器。調用者無須提供任何參數,因為不帶參數的便捷初始化器會沿著便捷初始化器鏈進行調用,直到遇到一個指定初始化器:


class Dog {
    var name : String
    var license : Int
    init(name:String, license:Int) {
        self.name = name
        self.license = license
    }
    convenience init(license:Int) {
        self.init(name:"Fido", license:license)
    }
    convenience init {
        self.init(license:1)
    }
}
let d = Dog
  

值得注意的是,本章之前介紹的初始化器可以做什麼,什麼時候做等原則依然有效。除了初始化屬性,只有當類的所有屬性都初始化完畢後,指定初始化器才能使用self。便捷初始化器是個委託初始化器,因此只有在直接或間接地調用了指定初始化器後,它才可以使用self(也不能設置不可變屬性)。

2.子類初始化器

介紹完指定初始化器與便捷初始化器並瞭解了它們之間的差別後,我們來看看當一個類本身是另一個類的子類時,關於初始化器的這些原則會發生什麼變化:

無聲明的初始化器

如果子類沒有聲明自己的初始化器,那麼其初始化器就會包含從父類中繼承下來的初始化器。

只有便捷初始化器

如果子類沒有自己的初始化器,那麼它就可以聲明便捷初始化器,並且與一般的便捷初始化器工作方式別無二致,因為繼承向self提供了便捷初始化器一定會調用的指定初始化器。

指定初始化器

如果子類聲明了自己的指定初始化器,那麼整個規則就會發生變化。現在,初始化器都不會被繼承下來!顯式的指定初始化器的存在阻止了初始化器的繼承。子類現在只擁有你顯式編寫的初始化器(不過有個例外,稍後將會介紹)。

現在,子類中的每個指定初始化器都有一個額外的要求:它必須要調用父類的一個指定初始化器,通過super.init(...)調用。此外,調用self的規則依然適用。子類的指定初始化器必須要按照如下順序調用執行:

1.必須確保該類(子類)的所有屬性都被初始化。

2.必須調用super.init(...),它所調用的初始化器必須是個指定初始化器。

3.滿足上面兩條之後,該初始化器才可以使用self,調用實例方法或訪問繼承的屬性。

子類中的便捷初始化器依然適用於上面列出的各種規則。它們必須調用self.init(...),直接或間接(通過便捷初始化器鏈)調用指定初始化器。如果沒有繼承下來的初始化器,那麼便捷初始化器所調用的初始化器必須顯式聲明在子類中。

如果指定初始化器沒有調用super.init(...),那麼在可能的情況下super.init()就會被隱式調用。如下代碼是合法的:


class Cat {
}
class NamedCat : Cat {
    let name : String
    init(name:String) {
        self.name = name
    }
}
  

在我看來,Swift的這個特性是錯誤的:Swift不應該使用這種秘密行為,即便這個行為看起來是「有益的」。我認為上述代碼不應該編譯通過;指定初始化器應該總是顯式調用super.init(...)。

重寫初始化器

子類可以重寫父類初始化器,但要遵循如下限定:

·簽名與父類便捷初始化器匹配的初始化器必須也是個便捷初始化器,無須標記為override。

·簽名與父類指定初始化器匹配的初始化器可以是指定初始化器,也可以是便捷初始化器,但必須要標記為override。在重寫的指定初始化器中可以通過super.init(...)調用被重寫的父類指定初始化器。

一般來說,如果子類有指定初始化器,那就不會繼承任何初始化器。不過,如果子類重寫了父類所有的指定初始化器,那麼子類就會繼承父類的便捷初始化器。

可失敗初始化器

只有在完成了自己的全部初始化任務後,可失敗指定初始化器才能夠調用return nil。比如,可失敗子類指定初始化器必須要完成所有子類屬性的初始化,在調用return nil前必須要調用super.init(...)(其實就是在實例銷毀前,必須要先構建出實例。不過,這是必要的,目的是確保父類能夠完成自己的初始化)。

如果可失敗初始化器所調用的初始化器是可失敗的,那麼調用語法並不會發生變化,也不需要額外的測試。如果被調用的可失敗初始化器失敗了,那麼整個初始化過程就會立刻失敗(而且會終止)。

針對重寫與委託的目的,返回隱式展開Optional的可失敗初始化器(init!)就像是個常規的初始化器(init)一樣。對於返回常規Optional(init?)的可失敗初始化器,有一些額外的限制:

·init可以重寫init?,反之則不行。

·init?可以調用init。

·init可以調用init?,方式是調用init並將結果展開(要使用感歎號,因為如果init?失敗了,程序將會崩潰)。

如下示例展示了合法的語法:


class A:NSObject {
    init?(ok:Bool) {
        super.init        // init? can call init
    }
}
class B:A {
    override init(ok:Bool) { // init can override init?
        super.init(ok:ok)!   // init can call init? using "!"
    }
}   

無論何時,子類初始化器都不能設置父類的常量屬性(let)。這是因為,當子類可以做除了初始化自己的屬性以及調用其他初始化器之外的事情時,父類已經完成了自己的初始化,子類已經沒有機會再初始化父類的常量屬性了。

下面是一些示例。首先來看這樣一個類,它的子類沒有聲明自己的顯式初始化器:


class Dog {
    var name : String
    var license : Int
    init(name:String, license:Int) {
        self.name = name
        self.license = license
    }
    convenience init(license:Int) {
        self.init(name:"Fido", license:license)
    }
}
class NoisyDog : Dog {
}
  

根據上述代碼,我們可以像下面這樣創建一個NoisyDog:


let nd1 = NoisyDog(name:"Fido", license:1)
let nd2 = NoisyDog(license:2)
  

上述代碼是合法的,因為NoisyDog繼承了父類的初始化器。不過,你不能像下面這樣創建NoisyDog:


let nd3 = NoisyDog // compile error
  

上述代碼是不合法的。雖然NoisyDog沒有聲明自己的屬性,它也沒有隱式初始化器;但它的初始化器是繼承下來的,其父類Dog也沒有可供繼承的隱式init()初始化器。

來看看下面這個類,其子類唯一的顯式初始化器是便捷初始化器:


class Dog {
    var name : String
    var license : Int
    init(name:String, license:Int) {
        self.name = name
        self.license = license
    }
    convenience init(license:Int) {
        self.init(name:"Fido", license:license)
    }
}
class NoisyDog : Dog {
    convenience init(name:String) {
        self.init(name:name, license:1)
    }
}
  

注意到NoisyDog的便捷初始化器是如何通過self.init(...)調用一個指定初始化器(正好是繼承下來的)來滿足其契約的。根據上述代碼,有3種方式可以創建NoisyDog,如下所示:


let nd1 = NoisyDog(name:"Fido", license:1)
let nd2 = NoisyDog(license:2)
let nd3 = NoisyDog(name:"Rover")
  

下面這個類的子類聲明了一個指定初始化器:


class Dog {
    var name : String
    var license : Int
    init(name:String, license:Int) {
        self.name = name
        self.license = license
    }
    convenience init(license:Int) {
        self.init(name:"Fido", license:license)
    }
}
class NoisyDog : Dog {
    init(name:String) {
        super.init(name:name, license:1)
    }
}
  

現在,NoisyDog的顯式初始化器是個指定初始化器。它通過在super調用指定初始化器滿足了契約。現在的NoisyDog阻止了所有初始化器的繼承;創建NoisyDog的唯一方式如下所示:


let nd1 = NoisyDog(name:"Rover")
  

最後,下面這個類的子類重寫了其指定初始化器:


class Dog {
    var name : String
    var license : Int
    init(name:String, license:Int) {
        self.name = name
        self.license = license
    }
    convenience init(license:Int) {
        self.init(name:"Fido", license:license)
    }
}
class NoisyDog : Dog {
    override init(name:String, license:Int) {
        super.init(name:name, license:license)
    }
}
  

NoisyDog重寫了父類所有的指定初始化器,因此它繼承了父類的便捷初始化器。有兩種方式可以創建NoisyDog:


let nd1 = NoisyDog(name:"Rover", license:1)
let nd2 = NoisyDog(license:2)
  

這些示例闡釋了你應該牢牢記住的主要規則。你可能不需要記住其他規則,因為編譯器會強制應用這些規則,並確保你所做的一切都是正確的。

1.必備初始化器

關於類初始化器還有一點值得注意:類初始化器前面可以加上關鍵字required,這意味著子類不可以省略它。反過來,這又表示如果子類實現了指定初始化器,從而阻止了繼承,那麼它必須要重寫該初始化器,參見如下示例:


class Dog {
    var name : String
    required init(name:String) {
        self.name = name
    }
}
class NoisyDog : Dog {
    var obedient = false
    init(obedient:Bool) {
        self.obedient = obedient
        super.init(name:"Fido")
    }
} // compile error
  

上述代碼無法編譯通過。init(name:)被標記為required,因此除非在NoisyDog中繼承或重寫init(name:),否則代碼編譯是通不過的。但我們不能繼承,因為通過實現NoisyDog的指定初始化器init(obedient:),繼承已經被阻止了。因此必須要重寫它:


class Dog {
    var name : String
    required init(name:String) {
        self.name = name
    }
}
class NoisyDog : Dog {
     var obedient = false
    init(obedient:Bool) {
        self.obedient = obedient
        super.init(name:"Fido")
    }
    required init(name:String) {
        super.init(name:name)
    }
}
  

注意,被重寫的必備初始化器並沒有標記override,但卻被標記了required,這樣就可以確保無論子類層次有多深都可以滿足需求。

我已經介紹過了將初始化器聲明為required的含義,但尚未介紹這麼做的原因,本章後面將會通過一些示例進行說明。

2.Cocoa的特殊之處

在繼承Cocoa類時,初始化器繼承規則可能會產生一些奇怪的結果。比如,在編寫iOS程序時,你肯定會聲明UIViewController子類。假設該子類聲明了一個指定初始化器。父類UIViewController中的指定初始化器是init(nibName:bundle:),因此為了滿足規則,你需要像下面這樣從指定初始化器中調用它:


class ViewController: UIViewController {
    init {
        super.init(nibName:"MyNib", bundle:nil)
    }
}
  

現在看來一切正常;不過,你會發現創建ViewController實例的代碼無法編譯通過了:


let vc = ViewController(nibName:"MyNib", bundle:nil) // compile error
  

只有聲明了自己的指定初始化器後,上面的代碼才能編譯通過;但現在並沒有這麼做。原因在於,通過在子類中實現指定初始化器,你阻止了初始化器的繼承!ViewController類過去會繼承UIViewController的init(nibName:bundle:)初始化器,但現在卻不是這樣。你還需要重寫該初始化器,即便實現只是調用被重寫的初始化器亦如此:


class ViewController: UIViewController {
    init {
        super.init(nibName:"MyNib", bundle:nil)
    }
    override init(nibName: String?, bundle: NSBundle?) {
        super.init(nibName:nibName, bundle:bundle)
    }
}
  

現在,如下實例化ViewController的代碼可以編譯通過了:


let vc = ViewController(nibName:"MyNib", bundle:nil) // fine
  

不過,現在又有一個令人驚詫之處:ViewController本身無法編譯通過了!原因在於還有一個施加於ViewController之上的必備初始化器,你還需要將其實現出來。之前你是不知道這一點的,因為當ViewController沒有顯式初始化器時,你會將必備初始化器繼承下來;現在,你又阻止了繼承。幸好,Xcode的Fix-It特性提供了一個樁實現;它什麼都沒做(事實上,如果調用,程序將會崩潰),不過卻滿足了編譯器的要求:


required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}
  

本章後面將會介紹該必備初始化器是如何應用的。

4.4.4 類析構器

只有類才會擁有析構器。它是個通過關鍵字deinit聲明的函數,後跟一對花括號,裡面是函數體。你永遠不會自己調用這個函數;它是當類的實例消亡時由運行時調用的。如果一個類有父類,那麼子類的析構器(如果有)會在父類的析構器(如果有)調用之前調用。

析構器的想法在於你可以在實例消亡前執行一些清理工作,或是向控制台打印一些日誌,證明操作執行順序是正確的。我將在第5章介紹內存管理主題時使用析構器。

4.4.5 類屬性與方法

子類可以重寫繼承下來的屬性。重寫的屬性必須要與繼承下來的屬性擁有相同的名字與類型,並且要標記為override(屬性與繼承下來的屬性不能只名字相同而類型不同,因為這樣就無法區分它們了)。需要遵循如下新規則:

·如果父類屬性是可寫的(存儲屬性或帶有setter的計算屬性),那麼子類在重寫時可以添加對該屬性的setter觀察者。

·此外,子類可以使用計算變量進行重寫。在這種情況下:

·如果父類屬性是存儲屬性,那麼子類的計算變量重寫就必須要有getter與setter。

·如果父類屬性是計算屬性,那麼子類的計算變量重寫就必須要重新實現父類實現的所有訪問器。如果父類屬性是只讀的(只有getter),那麼重寫可以添加setter。

重寫屬性的函數可以通過super關鍵字引用(讀或寫)繼承下來的屬性。

類可以有靜態成員,只需將其標記為static,就像結構體或枚舉一樣;還可以有類成員,標記為class。靜態與類成員都可以由子類繼承(分別作為靜態與類成員)。

從程序員的視角來看,靜態方法與類方法之間的主要差別在於靜態方法無法重寫;static就好像是class final的同義詞一樣。

比如,使用一個靜態方法表示狗叫:


class Dog {
    static func whatDogsSay -> String {
        return "woof"
    }
    func bark {
        print(Dog.whatDogsSay)
    }
}
  

子類現在繼承了whatDogsSay,但卻無法重寫。Dog的子類不能包含簽名相同的名為whatDogsSay的類方法或靜態方法實現。

下面使用一個類方法表示狗叫:


class Dog {
    class func whatDogsSay -> String {
        return "woof"
    }
    func bark {
        print(Dog.whatDogsSay)
    }
}
  

子類繼承了whatDogsSay,並且可以重寫,要麼作為類函數,要麼作為靜態函數:


class NoisyDog : Dog {
    override class func whatDogsSay -> String {
        return "WOOF"
    }
}
  

靜態屬性與類屬性之間的差別是類似的,不過還要再增加一條重要差別:靜態屬性可以是存儲屬性,而類屬性只能是計算屬性。

下面通過一個靜態類屬性來表示狗叫:


class Dog {
    static var whatDogsSay = "woof"
    func bark {
        print(Dog.whatDogsSay)
    }
}
  

子類繼承了whatDogsSay,但卻無法重寫;Dog的子類無法聲明類或靜態屬性whatDogsSay。

現在通過類屬性來表示狗叫。它不能是存儲屬性,因此只能使用計算屬性:


class Dog {
    class var whatDogsSay : String {
        return "woof"
    }
    func bark {
        print(Dog.whatDogsSay)
    }
}
  

子類繼承了whatDogsSay,並且可以通過類屬性或靜態屬性重寫它。不過,正如子類重寫的靜態屬性不能是存儲屬性一樣,這符合之前介紹的關於屬性重寫的原則:


class NoisyDog : Dog {
    override static var whatDogsSay : String {
        return "WOOF"
    }
}