Kotlin教學 | 從零開始學Kotlin | Kotlin入門 | CH8: 屬性(Property)

屬性

類別中宣告的變量稱作屬性。
Kotlin的屬性不只是單純儲存資料,還提供了:自定義訪問器(getter and setter)、支持欄位(Backing Fields)、支持屬性(Backing Properties)等。

由於屬性本身即是變量,因此也跟變數一樣需要先宣告再使用。

宣告語法

var <propertyName>[: <PropertyType>] [= <property_initializer>]
    [<getter>]
    [<setter>]

val <propertyName>[: <PropertyType>] [= <property_initializer>]
    [<getter>]
  • 屬性必須要初始化,有三種方式:
    • 使用<property_initializer>給予初始值。
    • 使用 <getter> 取得函數的回傳值給予。
    • 使用 lateinit關鍵字延遲給予。
  • : <PropertyType>:型別,如果可以透過type inference推斷型別,則可省略。
  • <getter> :關鍵字get()的特殊函數,用來返回值給屬性,可以宣告以自定義行為,每次從屬性取值時,自動呼叫這個函數。
    如果沒有需求,可以省略,Kotlin預設會自動隱式產生。
  • <setter>:關鍵字set()的特殊函數,用來設定值給屬性,可以宣告以自定義行為,每次賦值給屬性時,自動呼叫這個函數。
    如果沒有需求,可以省略。Kotlin預設會自動隱式產生。
  • val屬性只有<getter>,沒有<setter>
  • var屬性同時有<getter><setter>
var initialized = 1 // 類型為 Int,默認 getter 和 setter
// var allByDefault // 錯誤:需要顯式的初始化器,默認的 getter 和 setter 會被隱含

val inferredType = 1 // has type Int and a default getter

自定義訪問器(Getter and Setter)

Kotlin 允許你為屬性自定義訪問器 getter:get()和 setter:set()
這提供了在讀取或設定屬性值時進行附加操作的能力。

  • get():讀取屬性時自動呼叫此函數。
  • set():設定屬性時自動呼叫此函數。
  • val屬性只有get(),沒有set()
  • var屬性同時有get()set()
class Rectangle(var width: Int, var height: Int) {
    // 由於可以從函數實現的返回值推斷型別,因此省略型別宣告。
    var area get() = this.width * this.height
             set(value) {
                 this.height = value/this.width //change height
             }
}

var stringRepresentation: String
    get() = this.toString()
    set(value) {
        setDataFromString(value) // 解析字符串並賦值給其他屬性
    }

class Person {
    var name: String = "initial name"
        get() = field.toUpperCase() // 自定義 getter
        set(value) {
            field = "Name: $value" // 自定義 setter
        }
}

支持欄位(Backing Fields)

支持欄位(backing field)是一個自動生成的儲存空間,用於保存屬性的數據。
使用field關鍵字代表這個支持欄位和用來在get()set()中讀取或設定屬性的值。

需要field的原因是,如果在屬性的get()set()實現內,再讀取或設定屬性,會導致再呼叫get()set(),接著又再讀取或設定屬性,又再呼叫get()set(),如此get()set()無窮遞迴。
因此需要有一個可以讀取或設定屬性,但是不會觸發再次呼叫get()set()的機制。
field就是扮演這樣的腳色,在屬性的get()set()實現內,讀取或設定屬性,但是不會觸發再次呼叫get()set()

class Counter {
    var count: Int = 0
        set(value) {
            if (value >= 0) 
                field = value // 使用 field 設定count的數據,避免觸發set()
             // counter = value // ERROR StackOverflow: 直接 'counter' 導致set()無窮遞迴
        }
}

自動產生的支持欄位

不是所有的屬性都會有一個支持欄位。這取決於屬性如何被定義和使用。

支持欄位自動產生的條件

  1. 默認訪問器的使用:如果屬性使用了默認的 get()set() 實現,Kotlin 將為該屬性自動生成一個支持欄位。
  2. 自定義訪問器中的引用:如果在自定義的 get()set() 中通過 field 引用了支持欄位,Kotlin 會為該屬性生成一個支持欄位。

支持欄位不會自動產生的情況

  • 如果屬性的 getter 和 setter 都是自定義的,並且沒有使用 field 標識符,則不會為該屬性生成支持欄位。
class Example(val size: Int) {
    // 這個屬性沒有使用支持欄位,因為它的值是通過計算獲得的
    val isEmpty: Boolean
        get() = this.size == 0
}

支持屬性(Backing Properties)

當默認的支持欄位(backing field)無法滿足特定場景時,可以使用支持屬性(backing property)。
支持屬性允許開發者在屬性讀取和設定時執行更複雜的邏輯。

支持屬性的應用場景

  • 當屬性的讀取和設定需要額外的邏輯處理。
  • 當需要延遲屬性的初始化,或屬性的初始化依賴於某些條件。
  • 當需要控制屬性的訪問權限,例如使 setter 為私有。
class User {
    private var _email: String? = null
    val email: String
        get() {
            if (_email == null) {
                _email = "email@example.com" // 延遲初始化
            }
            return _email!!
        }
}

延遲初始化屬性

對於無法在初始化時就確定其值的屬性,Kotlin 提供了 lateinit 關鍵字來聲明延遲初始化屬性(late-initialized properties)。

一般情況,屬性必須要給予初始值,讓物件在創造時初始化。
但是會遇到某些應用場景,在物件初始化時,還不能確定屬性的初始值。
這種情況經常發生於依賴注入或單元測試的設置方法中。

要使用lateinit 必須滿足以下條件:

  • 屬性必須是 var(可變的)。
  • 屬性不能在主構造函數中聲明。
  • 屬性不能有自定義訪問器
  • 屬性的類型必須是非空的,且不能是基本型別
class MyTest {
    lateinit var subject: TestSubject

    @SetUp fun setup() {
        // 在測試設置方法中初始化 subject
        subject = TestSubject()
    }

    @Test fun test() {
        // 直接訪問 subject,無需空檢查
        subject.method()
    }
}

檢查遲初始化屬性是否已初始化

要檢查一個 lateinit var 是否已被初始化,可以使用 .isInitialized 屬性。
這種檢查只適用於在同一類型中、外部類型中或相同文件的頂層聲明的屬性。

if (::subject.isInitialized) {
    println(subject)
}

Reference

https://kotlinlang.org/docs/properties.html

發佈留言