#26 init的那些小事 part1

Jeff Lin
19 min readSep 6, 2023

--

前言:
前陣子不知道為什麼,突然想打很多init看看,結果如下:

class Dog{
func run(){ print("run") }
init(){ print("汪汪叫") }
}

class Puppy1:Dog{
override func run() { print("puppy1 run") }
override init() { print("一號小狗汪汪叫") }
}


class Puppy2:Puppy1{
override func run() { print("puppy2 run") }
override init() { print("二號小狗汪汪叫") }
}


class Puppy3:Puppy2{
override init(){ print("三號小狗汪汪叫") }
}

let dog = Dog() //印出汪汪叫
let puppy1 = Puppy1() //印出一號小狗汪汪叫、汪汪叫
let puppy2 = Puppy2() //印出二號小狗汪汪叫、一號小狗汪汪叫、汪汪叫
let puppy3 = Puppy3() //印出三號小狗汪汪叫、二號小狗汪汪叫、一號小狗汪汪叫、汪汪叫

我百思不得其解,明明都override init,為什麼子類別除了印出自己的init內容,卻還會印出父類別初始化的內容,盯著螢幕發呆了一下,表情大概像這樣:

正在考慮直接背多分起來,還是探究原因,天人交戰一下,還是決定來場init的西天取經之旅。

1. init的意義
2. designated initializers與convenience initializers
3. 繼承
4. 狗狗事件
5. convenience initializer不能被覆寫

init的意義:

在Swift裡,當實例被初始化時,裡面的屬性一定要有內容才能被初始化,當我們沒有設定值時,就要靠Initializer來將屬性賦值。

屬性有值的情況:

可以發現在建立Dog時使用圓括弧,表示在建立Dog物件時,有呼叫init( ),然而我們卻沒有定義init( ),原因是因為在沒有自定義Initializer時,系統會使用隱含初始化 (implicit initializer),即是init( )來初始化物件。

class Dog{ }
let dog = Dog()

當屬性有值時,不自定義init的情況下,系統也是使用預設的init來啟動Dog物件。

class Dog{ let name = "旺福" }
let dog = Dog()

屬性沒有值的情況:

建立一個Monkey的類別,並建立幾個屬性。當Monkey的屬性不給值,這時系統就會要求要自定義init賦值給屬性,以完成物件的初始化,這時的Initializer 就稱為 designated initializer

接著初始化Monkey時出現的建構子就會是自定義的,而不是原本的Monkey()。

自定義的initializer會取代掉原本的implicit initializer
class Monkey{
var name:String
var color:String
init (name:String, color:String){
self.name = name
self.color = color
}
}

let monkey = Monkey(name: "孫悟空", color: "黃色")

目前為止都還算簡單,但開始討論到繼承時,事情開始從複雜變成難以理解🥲,接下來開始探討繼承後會發生什麼事。

designated initializers與convenience initializers:

在討論到init的繼承前,要先知道兩個初始化器:designated initializers與convenience initializers。

designated initializers:

Designated initializers are the primary initializers for a class. A designated initializer fully initializes all properties introduced by that class and calls an appropriate superclass initializer to continue the initialization process up the superclass chain.

Designated initializers是class裡主要的初始化器,功能是將所有自己的class新增的屬性賦值,以及呼叫父類別的initializer將繼承來的屬性賦值。

convenience initializers:

Convenience initializers are secondary, supporting initializers for a class. You can define a convenience initializer to call a designated initializer from the same class as the convenience initializer with some of the designated initializer’s parameters set to default values. You can also define a convenience initializer to create an instance of that class for a specific use case or input value type.

Convenience initializers為次要的初始化器,他有幾個使用條件:

  1. A convenience initializer must call another initializer from the same class.
  2. A convenience initializer must ultimately call a designated initializer.

他的呼叫一定要呼叫自己class裡的initializer,且convenience initializer 最終一定會呼叫到designated initializer,呼叫脈絡如下:

使用他的情境大多是用於調整參數時使用,如下:

class File {
var filename: String
var data: String

init(filename: String, data: Any) {
self.filename = filename
self.data = String(describing: data)
}

convenience init(data:Double){
self.init(filename: "台灣秘密檔案", data: data)
}
}

先把convenience init comment掉。

從圖中可知最一開始File唯一的建構子只有init(filename:data:)

但如果今天我只想針對特別的參數做更改,這時候為了調整參數,就可以使用convenience initializer。

根據convenience initializer呼叫的規則,他必須要呼叫class內的initializer,因此他在這裡呼叫init(filename:data:)

可以從圖中看到,建構子除了原本的init(filename:data:),也出現init(data:)可以選擇,這讓初始化的方法更方便更簡潔。

當然,想要有多個建構子選擇,也可以選擇最原始的方始:寫一堆init。

但是這樣重複的code會太多,而且看起來會有點亂,所以convenience initializer提供更簡單的方式來建立多個建構子。

繼承:

在物件繼承的過程中,Two-Phase Initialization 要遵守,以下會附上原文說明。

目的:避免屬性在初始化前就被存取,也避免值被設成建構子預期以外的值。

第一階段:每個儲存屬性都會被賦值

  • A designated or convenience initializer is called on a class.
    designated與convenience initializer被呼叫
  • Memory for a new instance of that class is allocated. The memory isn’t yet initialized.
    劃分出一塊記憶體空間來放置該物件,但記憶體尚未初始化
  • A designated initializer for that class confirms that all stored properties introduced by that class have a value. The memory for these stored properties is now initialized.
    designated initializer把”自己的屬性”都賦值後,屬性的記憶體初始化
  • The designated initializer hands off to a superclass initializer to perform the same task for its own stored properties.
    designated initializer呼叫父類別的建構子,將繼承來的屬性初始化 。
    換言之,要初始化繼承來的屬性,要使用父類別的init。
  • This continues up the class inheritance chain until the top of the chain is reached.
    designed initializer會啟動上游(父類別)的designed initializer,每個父類別會再啟動自己上游的designed initializer。
  • Once the top of the chain is reached, and the final class in the chain has ensured that all of its stored properties have a value, the instance’s memory is considered to be fully initialized, and phase 1 is complete.
    當所有的儲存屬性都有值後,物件的記憶體被初始化,phase 1 完成。

第二階段:每個class可以開始針對屬性去做最後修改

  • Working back down from the top of the chain, each designated initializer in the chain has the option to customize the instance further. Initializers are now able to access self and can modify its properties, call its instance methods, and so on.
  • Finally, any convenience initializers in the chain have the option to customize the instance and to work with self.
    建構子可以開始使用self去修改自己的property,也可以呼叫自己的方法。

統整上述,可以歸納出一個結論:
先將自己定義的屬性賦值 > 將繼承的屬性賦值 > 修改繼承而來的屬性

  1. 將自己定義的屬性賦值:

首先有一輛Vehicle類別的物件,並設定屬性,再藉由自定義initializer將沒有值的屬性賦值。

class Vehicle {
var numberOfWheels:Int
var size = "Large"
var description: String {
return "\(numberOfWheels) wheel(s)"
}
//因為numberOfWheels沒有初始值,所以在init裡將該屬性賦值
init(numberOfWheels:Int) {
self.numberOfWheels = numberOfWheels
}
}

接著定義一輛腳踏車,繼承Vehicle,並建立一個新的屬性叫做color


class Bicycle: Vehicle {
var color:String
}

此時會跳出error,說明要自定義init將color初始化。

class Bicycle: Vehicle {
var color:String
}

init(color:String) {
self.color = color //報錯
}

2. 將繼承的屬性賦值:

但這樣寫會發生一個錯誤:雖然初始化了自己的屬性,但繼承來的屬性卻還沒有初始化。所以必須要在自己定義的init裡呼叫父類別的init來初始化繼承下來的屬性

完整寫法如下:

class Bicycle: Vehicle {
var color:String

init(numberOfWheels: Int, color:String) {
self.color = color
super.init(numberOfWheels: numberOfWheels)
}
}

3. 修改繼承而來的屬性:

將腳踏車造出來後,因為有繼承Vehiclesize屬性,而size預設值是Large,所以腳踏車的屬性size也是Large

這不符合我對腳踏車的期待,因此我要對繼承來的屬性做修改,這樣子寫立馬報錯:

class Bicycle: Vehicle {
var color:String

init(numberOfWheels: Int, color:String) {
self.size = "Middle" //報錯
self.color = color
super.init(numberOfWheels: numberOfWheels)

}
}

原因是因為,要存取任何屬性前都要先初始化屬性。因此存取size屬性之前必須要先初始化他,而這個屬性是繼承來的,因此初始化他的方法要呼叫父類別的init來初始化這個屬性。

所以self.size=“Middle”要擺在super.init(numberOfWheels:)之後

因此更改如下:

class Bicycle: Vehicle {
var color:String
init(numberOfWheels: Int, color:String) {
self.color = color
super.init(numberOfWheels: numberOfWheels)
self.size = "Middle"
}
}

所以要先將所有屬性都賦值,才能去存取、修改屬性,甚至是呼叫方法。

再看一個例子,全部的屬性都賦值前不能存取屬性或呼叫方法

class Doggie3 {
var name: String
var license: Int

init(name: String = "Dog5", license: Int = 5) {
self.name = name
//bark()
// 編譯錯誤,要等所有屬性都有值後(包括繼承下來與自定義的屬性),此物件才會被初始化,才能呼叫自己的method,所以應該要放到self.license = license以後
self.license = license
}

func bark() {
print("woof")
}
}

self.license 被賦值前,物件還不會被初始化,此時是不能呼叫到自己的方法bark() 。所以要將bark() 移到self.license = license 後,才能正確呼叫這個方法。

狗狗事件:

回到最一開始的狗狗事件,為什麼繼承下來的狗狗會亂吠?

我再以例子說明一下:

首先,先建立Vehicle 物件

class Vehicle {
var numberOfWheels = 0
var description: String {
return "\(numberOfWheels) wheel(s)"
}
}

接著建立Hoverboard 物件,並繼承Vehicle 。當Hoverboard繼承Vehicle後,自定義designated initializer init(color: String)將屬性color賦值並初始化屬性。

class Hoverboard: Vehicle {
var color: String
init(color: String) {
self.color = color
}
override var description: String {
return "\(super.description) in a beautiful \(color)"
}
}

根據剛剛的繼承規則,將自己的屬性賦值後,要呼叫父類別的初始化方法,將繼承下來的屬性初始化,因此在init(color: String)裡面要呼叫super.init()

class Hoverboard: Vehicle {
var color: String
init(color: String) {
self.color = color
super.init()
}
override var description: String {
return "\(super.description) in a beautiful \(color)"
}
}

有趣的來了,根據文件說明,如果呼叫super.init()後沒有修改任何繼承下來的屬性,而父類別的初始化方法也是無參數,則可以省略super.init()。

If a subclass initializer performs no customization in phase 2 of the initialization process, and the superclass has a synchronous, zero-argument designated initializer, you can omit a call to super.init() after assigning values to all of the subclass’s stored properties. If the superclass’s initializer is asynchronous, you need to write await super.init() explicitly.

我在繼承後因為沒有更改父類別的屬性numberOfWheels ,所以就不用寫super.init(),系統會自動呼叫,結果就變這樣:

class Hoverboard: Vehicle {
var color: String
init(color: String) {
self.color = color
// super.init() implicitly called here
}
override var description: String {
return "\(super.description) in a beautiful \(color)"
}
}

那這樣跟狗狗事件有什麼關係?

Puppy1繼承Dog,自定義一個override init()

我沒有在override init()裡呼叫super.init(),系統也會自動呼叫,原因有二:

  1. 再自定義的init裡一定會呼叫super.init(…),確保所有的屬性都能初始化成功。
  2. 當父類別的初始化方法是不帶參數的super.init(),且沒有去修改任何繼承下來的屬性,我們就不用自己呼叫,系統會自己偷偷呼叫。

這導致就算我沒有呼叫super.init(),我的Puppy1物件初始化後,印出一號小狗汪汪叫,接著會在執行super.init()而印出汪汪叫

class Dog{
func run(){ print("run") }
init(){ print("汪汪叫") }
}

class Puppy1:Dog{
override func run() { print("puppy1 run") }
override init() { print("一號小狗汪汪叫") }
}

convenience initializer不能被覆寫:

最後再記錄一下,若子類別定義一個一個跟父類別的convenience initializer一樣的initializer時,不用寫override。

子類別Car繼承Vehicle,他定義一個跟父類別一樣的convenience initializer convenience init() ,但卻不用寫override。

class Vehicle {
var numberOfWheels: Int
init(numberOfWheels: Int) {
self.numberOfWheels = numberOfWheels
}
convenience init() {
self.init(numberOfWheels: 4)
}
}


class Car: Vehicle {
var brand: String

init(brand: String) {
self.brand = brand
super.init(numberOfWheels: 4)
}

convenience init() {
self.init(brand: "Unknown")
}
}

--

--

Jeff Lin
Jeff Lin

Written by Jeff Lin

一個喜歡彈吉他、寫 Swift 的人,所以手指沒有閒下來的一天

No responses yet