天天看點

Kotlin 從學習到 Android 第七章 屬性和字段

聲明屬性

在 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() // there's no 'new' keyword in Kotlin
    result.name = address.name // accessors are called
    result.street = address.street
    // ...
    return result
}
           

getter 和 setter

完整的屬性聲明語句如下:

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

其中初始化值,getter 和 setter 是可選的。如果系統可以根據初始化值(或 getter 的傳回值)推斷出屬性的類型,那麼屬性類型也是可選的,例如:

var allByDefault: Int? // 錯誤: 需要明确初始化值, 隐含着預設的 getter and setter
var initialized = 1 // 可推導出資料類型為 Int, 含有預設的 getter and setter
           

隻讀屬性和可修改屬性的完整聲明不同,它沒有 setter :

val simple: Int? // 有屬性類型 Int, 預設的 getter, 必須在構造函數中初始化
val inferredType = 1 // 屬性類型為 Int 并有一個預設的 getter
           

我們也可以自定義 getter 和 setter ,隻需要在屬性右邊像聲明一般函數那樣聲即可:

var stringRepresentation: String
    get() = this.toString()
    set(value) {
       ...
    }
           

按照 Kotlin 的文法規範,setter 的參數名一般都是 value ,當然你也可以定義一個其它名稱的參數。

從 Kotlin 1.1 開始,如果一個屬性的類型可以從 getter 推斷出來,那麼在聲明時可以省略屬性類型的聲明:

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

如果你隻想更改 getter 或 setter 的通路權限或添加注解,而不改變它們的預設實作方式,那麼你可以像下面這樣不寫它們的函數體:

var setterVisibility: String = "abc"
    private set // 将 setter 修改為 private 權限

var setterWithAnnotation: Any? = null
    @Inject set // 為 setter 添加注解
           

Backing Fields

Kotlin 中的類不能含有 fields 。然而,有時當我們使用自定義存取器的時候需要 backing fields 。是以,Kotlin 提供了一個自動的 backing fields ,可以通過關鍵字 field 進行存取:

var counter = 0 // 初始化值被直接寫給 backing fields
    set(value) {
        if (value >= 0) field = value
    }
           

注意: field 關鍵字隻能用在屬性的存取上。

如果屬性至少使用了一個預設的存取器或者這個屬性在自定義的存取器中聲明了 field ,那麼系統會自動為它生成一個 backing field 。

例如,下面的情況将不會有 backing field (因為 isEmpty 是隻讀的沒有 setter ,自定義的 getter 中又沒有聲明 field ):

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

如果将 val 換成 var ,雖然自定義的 getter 沒有聲明 field ,但是 isEmpty 屬性有預設的 setter ,是以有 backing field:

var isEmpty: Boolean = true
        get() = this.size == 0
           

下面可以對以上結論進行證明:

fun main(args: Array<String>) {
    var test: Test = Test()
    var fields: Array<Field> = test.javaClass.declaredFields ;
    for(field in fields){
        println(field.name)
    }
}

class Test {
    var counter = 0 // the initializer value is written directly to the backing field
        set(value) {
            if (value >= 0) field = value
        }

    var isEmpty: Boolean = true
        get() = 1 == 0

    val isNumber: Boolean
        get() = 1 == 0
}
           

列印結果:

counter
isEmpty
           

Backing Properties

如果你想做一些操作,且這些操作不适合具有 backing field 的性質,那麼你可以這麼做:

private var _table: Map<String, Int>? = null
public val table: Map<String, Int>
    get() {
        // 這裡的操作不希望屬于 backing filed,
        if (_table == null) {
            _table = HashMap() 
        }
        return _table ?: throw AssertionError("Set to null by another thread")
    }
           

這就像 java 中私有屬性的預設 getter 和 setter ,這樣就沒有其它函數可以介入對這個屬性的操作了。

編譯時常量

編譯時可以确定值的屬性可以用 const 修飾來表明它是編譯時常量,這樣的屬性必須符合以下的條件:

  • 對象的頂級屬性或成員
  • 以 String 類型或原始類型初始化
  • 沒有自定義的 getter

編譯時常量可以用在注解中:

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

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

遲初始化屬性

一般情況下,如果屬性被聲明為非 null 的,那麼就必須在構造函數中初始化。然而,這樣十分不友善。例如,屬性可能通過注解,或單元測試中的方法初始化。在這些情況下,你不能實作在構造函數中初始化,但是你仍然希望在這個屬性在所在的類中被安全的使用,即不用判斷非 null 。

為了解決這種情況,你可以使用 lateinit 來修飾這個屬性:

public class MyTest {
    lateinit var subject: TestSubject

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

    @Test fun test() {
        subject.method()  // dereference directly
    }
}
           

lateinit 的使用條件必須滿足:

  • 類體中用 var 聲明的屬性且不在主構造函數中
  • 沒有自定義的 getter 或 setter
  • 屬性類型非 null
  • 不能為原始類型

在一個 lateinit 屬性初始化前通路它将會抛出異常:

kotlin.UninitializedPropertyAccessException: lateinit property subject has not been initialized
           

覆寫屬性

見第六章

委托屬性

最常見的屬性隻是從(可能寫入)一個支援字段中讀取。另一方面,使用自定義getter和setter方法可以實作屬性的任何行為。在介于兩者之間,有一些共同的模式來說明一個屬性是如何工作的。例如:lazy values 、根據 key 從 map 中讀取值、通路資料庫、通知偵聽器通路等等。

這些常見的行為可以作為使用委托屬性的庫來實作。