Kotlin研發第七彈——屬性與字段

屬性與字段

聲明屬性

Kotlin的類可以有屬性。 屬性可以用關鍵字var 聲明爲可變的,否則使用只讀關鍵字val

class Address {
    var name: String = ……
    var street: String = ……
    var city: String = ……
    var state: String? = ……
    var zip: String = ……
}

要使用一個屬性,只要用名稱引用它即可,就像 Java 中的字段:

fun copyAddress(address: Address): Address {
    val result = Address() // Kotlin 中沒有“new”關鍵字
    result.name = address.name // 將調用訪問器
    result.street = address.street
    // ……
    return result
}

Getters 與 Setters

聲明一個屬性的完整語法是

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

其初始器(initializer)、getter 和 setter 都是可選的。屬性類型如果可以從初始器 (或者從其 getter 返回值,如下文所示)中推斷出來,也可以省略。

例如:

var allByDefault: Int? // 錯誤:需要顯式初始化器,隱含默認 getter 和 setter
var initialized = 1 // 類型 Int、默認 getter 和 setter

一個只讀屬性的語法和一個可變的屬性的語法有兩方面的不同:1、只讀屬性的用 val開始代替var 2、只讀屬性不允許 setter

val simple: Int? // 類型 Int、默認 getter、必須在構造函數中初始化
val inferredType = 1 // 類型 Int 、默認 getter

我們可以爲屬性定義自定義的訪問器。如果我們定義了一個自定義的 getter,那麼每次訪問該屬性時都會調用它 (這讓我們可以實現計算出的屬性)。以下是一個自定義 getter 的示例:

val isEmpty: Boolean
    get() = this.size == 0

如果我們定義了一個自定義的 setter,那麼每次給屬性賦值時都會調用它。一個自定義的 setter 如下所示:

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

按照慣例,setter 參數的名稱是 value,但是如果你喜歡你可以選擇一個不同的名稱。

自 Kotlin 1.1 起,如果可以從 getter 推斷出屬性類型,則可以省略它:

val isEmpty get() = this.size == 0 // 具有類型 Boolean

如果你需要改變一個訪問器的可見性或者對其註解,但是不需要改變默認的實現, 你可以定義訪問器而不定義其實現:

var setterVisibility: String = "abc"
    private set // 此 setter 是私有的並且有默認實現

var setterWithAnnotation: Any? = null
    @Inject set // 用 Inject 註解此 setter

幕後字段

在 Kotlin 類中不能直接聲明字段。然而,當一個屬性需要一個幕後字段時,Kotlin 會自動提供。這個幕後字段可以使用field標識符在訪問器中引用:

var counter = 0 // 注意:這個初始器直接爲幕後字段賦值
    set(value) {
        if (value >= 0) field = value
    }

field 標識符只能用在屬性的訪問器內。

如果屬性至少一個訪問器使用默認實現,或者自定義訪問器通過 field 引用幕後字段,將會爲該屬性生成一個幕後字段。

例如,下面的情況下, 就沒有幕後字段:

val isEmpty: Boolean
    get() = this.size == 0

幕後屬性

如果你的需求不符合這套“隱式的幕後字段”方案,那麼總可以使用 幕後屬性(backing property)

private var _table: Map<String, Int>? = null
public val table: Map<String, Int>
    get() {
        if (_table == null) {
            _table = HashMap() // 類型參數已推斷出
        }
        return _table ?: throw AssertionError("Set to null by another thread")
    }

從各方面看,這正是與 Java 相同的方式。因爲通過默認 getter 和 setter 訪問私有屬性會被優化,所以不會引入函數調用開銷。

編譯期常量

已知值的屬性可以使用 const 修飾符標記爲 編譯期常量。 這些屬性需要滿足以下要求:

這些屬性可以用在註解中:

const val SUBSYSTEM_DEPRECATED: String = "This subsystem is deprecated"

@Deprecated(SUBSYSTEM_DEPRECATED) fun foo() { …… }

延遲初始化屬性與變量

一般地,屬性聲明爲非空類型必須在構造函數中初始化。 然而,這經常不方便。例如:屬性可以通過依賴注入來初始化, 或者在單元測試的 setup 方法中初始化。 這種情況下,你不能在構造函數內提供一個非空初始器。 但你仍然想在類體中引用該屬性時避免空檢查。

爲處理這種情況,你可以用 lateinit 修飾符標記該屬性:

public class MyTest {
    lateinit var subject: TestSubject

    @SetUp fun setup() {
        subject = TestSubject()
    }

    @Test fun test() {
        subject.method() // 直接解引用
    }
}

該修飾符只能用於在類體中的屬性(不是在主構造函數中聲明的 var 屬性,並且僅當該屬性沒有自定義 getter 或 setter 時),而自 Kotlin 1.2 起,也用於頂層屬性與局部變量。該屬性或變量必須爲非空類型,並且不能是原生類型。

在初始化前訪問一個 lateinit 屬性會拋出一個特定異常,該異常明確標識該屬性被訪問及它沒有初始化的事實。

檢測一個 lateinit var 是否已初始化(自 1.2 起)

要檢測一個 lateinit var 是否已經初始化過,請在該屬性的引用上使用 .isInitialized

if (foo::bar.isInitialized) {
    println(foo.bar)
}

此檢測僅對可詞法級訪問的屬性可用,即聲明位於同一個類型內、位於其中一個外圍類型中或者位於相同文件的頂層的屬性。

覆蓋屬性

參見覆蓋屬性

委託屬性

最常見的一類屬性就是簡單地從幕後字段中讀取(以及可能的寫入)。 另一方面,使用自定義 getter 和 setter 可以實現屬性的任何行爲。 介於兩者之間,屬性如何工作有一些常見的模式。一些例子:惰性值、 通過鍵值從映射讀取、訪問數據庫、訪問時通知偵聽器等等。

這些常見行爲可以通過使用委託屬性實現爲庫。

跳轉上一章 Kotlin研發第六彈——類與繼承

跳轉下一章 Kotlin研發第八彈——可見性修飾符

發佈了34 篇原創文章 · 獲贊 20 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章