Kotlin 學習筆記 (10) —初始化

Andy Lu
7 min readSep 9, 2020

本章討論的是類別及其屬性的初始化。

初始化變數、屬性或類別實例,就是賦予初始值,變成可用狀態。

理論上,為物件分配記憶體就是產生實體物件,設定物件的值便是初始化物件。

實務上,通常初始化是指為了讓變數、屬性或類別實例可用而做的一切工作。產生實體,傾向於僅僅指「建立一個類別實例」。

建構函數

class Player(_name: String,
var healthPoints: Int,
var isBlessed: Boolean,
private var isImmortal: Boolean ){
var name = _name
get() = field.capitalize()
private set(value){
field = value.trim()
}
}
  • 此建構函數含有四個參數,其中 _name 由建構函數傳入之後,立刻指定給變數 name,並提供相對應的 gettersetter
  • 對於 _name 以外的建構函數參數,都要指定是可寫還是唯讀。透過 valvar 關鍵字指定參數後,便定義了類別屬性。
  • 無論是 val 屬性還是 var 屬性,建構函數都需要對應參數的引數。每個屬性都隱式地賦予對應的引數。

次建構函數

定義類別時,可以定義多個建構函數。某一個類別定義了主建構函數,該類別的所有實例都要透過主建構函數來建立,若增加一個次建構函數,則此建構函數需要直接或間接的呼叫主建構函數。

class Player(_name:String,
var healthPoints: Int,
var isBlessed: Boolean,
private var isImmortal: Boolean ){
var name = _name
get() = field.capitalize()
private set(value){
field = value.trim()
}
constructor(name:String) : this(name,
healthPoints = 100,
isBlessed = true,
isImmortal = false)
...
}

上例中,次建構函數用關鍵字 constructor 定義,可以發現傳入次建構函數的引數只有一個(name:String),接者,次建構函數利用 this 呼叫主建構函數,並將 name 帶入。

呼叫次建構函數

fun main(){
val player = Player("Madrigal")
}

次建構函數的初始化邏輯

我們可以在次建構函數的後方,以大括弧包住需要初始化的邏輯。

class Player(_name:String,
var healthPoints: Int,
var isBlessed: Boolean,
private var isImmortal: Boolean ){
var name = _name
get() = field.capitalize()
private set(value){
field = value.trim()
}
constructor(name:String) : this(name,
healthPoints = 100,
isBlessed = true,
isImmortal = false){
if(name.toLowerCase() == "kar") healthPoints = 40;
}

...
}
  • 請注意,次建構函數無法定義類別屬性,類別屬性只能定義於主建構函數。

預設參數

由上方的範例得知,可以給予建構函數預設參數,將參數後方以等號指定。無論是主建構函數,亦或是次建構函數,都能夠提供建構函數預設值。

具名引數

呼叫建構函數時,需要將各個建構函數的參數依序帶入,若數量較多,有時候會不容易分清楚。

那麼,可以使用具名引數,在呼叫建構子的時候,各個引數更具有識別度。

//具名引數
val player = Player(name = "Madrigal",
healthPoints = 100,
isBlessed = true,
isImmortal = false)
//預設引數
val player = Player("Madrigal", 100, true, false)

初始化區塊

除了主次建構函數,還可以為類別定義一個初始化區塊。設定變數或值,以及執行有效性檢查。當建立類別實例時,便會執行初始化區塊的程式碼。

初始化區塊以 init{ } 定義。

class Player(_name:String,
var healthPoints: Int,
var isBlessed: Boolean,
private var isImmortal: Boolean ){
var name = _name
get() = field.capitalize()
private set(value){
field = value.trim()
}
init{
require(healthPoints > 0, {"healthPoints must be greater than zero"})
require(name.isNotBlank(), {"Player must have a name"})
}


constructor(name:String) : this(name,
healthPoints = 100,
isBlessed = true,
isImmortal = false){
if(name.toLowerCase() == "kar") healthPoints = 40;
}
...
}

初始化順序

  1. 主建構函數
  2. 類別層級的屬性設定值
  3. 初始化區塊
  4. 次建構函數裡的屬性設定值與函式呼叫

其中 2、3 是根據定義的先後順序。

→ 由於類別層級的屬性設定值與初始化區塊是根據其定義的順序來決定初始化的順序,使用上一定要特別小心。

延遲初始化

關鍵字: lateinit

在類別屬性的前方加上 lateinit,即可延遲至使用之前初始化,不需要在定義時就完成初始化。

class Player{
lateinit var alignment: String

fun determineFate(){
alignment = "Good"
}

fun proclaimFate(){
if(::alignment.isInitialized) println(alignment)
}
}
  • 若使用 lateinit 定義屬性,卻未在使用之前設定初值,則會拋出 UninitializedPropertyAccessException 異常。
  • 可以使用 isInitialized 查詢是否初始化,不過如果針對每個延遲初始化的變數都進行此檢查,便和使用一個可空類型變數沒什麼區別。

惰性初始化

在 Kotlin,惰性初始化是透過一種叫做代理的機制來實作。代理負責約定該如何初始化屬性。

代理關鍵字 by,設定惰性初始化:在類別屬性加上 by lazy { },其中 { }為 lambda 函數。

class Player{
val hometown by lazy{
selectHomeTown()
}
...
private fun selectHometown() = ...
}
  • hometown 只會在初次使用時,才會初始化。
  • 惰性初始化雖然有用,但程式碼實作稍顯繁瑣,建議在處理計算密集型任務時使用。

小結

有兩種方式初始化類別的屬性,1. 建構函數引數,2. 在屬性定義時初始化。

建構函數可以同時有主次建構函數,不過次建構函數無法定義屬性,只能根據主建構函數的屬性來做操作。

每一個類別屬性定義時,必須給予初始值。若無法在定義時就提供初始值,可以使用 lateinit 關鍵字,延後至使用該屬性之前定義。

若屬性的初始值需要繁複的工作,例如:讀寫檔案,那麼可以使用惰性初始化,在屬性的後方加上 by lazy {}

除了主次建構函數初始化屬性外,還可以使用初始化區塊。在初始化區塊中,將需要初始化,或是狀態檢查的內容擺進去,如此就可以在建構類別之時,進行初始化或狀態檢查。不過要特別注意的是,初始化區塊的建構順序,與類別屬性的建構順序會根據定義的順序而有所不同。

如果本篇文章有幫助到您,請👏🏻鼓勵我。

謝謝🙇🏻

參考

KotlinLang: Classes

Kotlin 權威2.0:Android 專家養成術 — 第十三章:初始化

--

--

Andy Lu
Andy Lu

Written by Andy Lu

Android/Flutter developer, Kotlin Expert, like to learn and share.

No responses yet