# 委托屬性
[TOC]
有一些常見的屬性類型,雖然我們可以在每次需要的時候手動實現它們,但是如果能夠為大家把他們只實現一次并放入一個庫會更好。例如包括:
* 延遲屬性(lazy properties): 其值只在首次訪問時計算;
* 可觀察屬性(observable properties): 監聽器會收到有關此屬性變更的通知;
* 把多個屬性儲存在一個映射(map)中,而不是每個存在單獨的字段中。
為了涵蓋這些(以及其他)情況,Kotlin 支持 _委托屬性_:
```kotlin
class Example {
var p: String by Delegate()
}
```
語法是: `val/var <屬性名>: <類型> by <表達式>`。在 *by*{:.keyword} 后面的表達式是該 _委托_,因為屬性對應的 `get()`(與 `set()`)會被委托給它的 `getValue()` 與 `setValue()` 方法。屬性的委托不必實現任何的接口,但是需要提供一個 `getValue()` 函數(與 `setValue()`——對于 *var*屬性)。
例如:
```kotlin
import kotlin.reflect.KProperty
class Delegate {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "$thisRef, thank you for delegating '${property.name}' to me!"
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("$value has been assigned to '${property.name}' in $thisRef.")
}
}
```
當我們從委托到一個 `Delegate` 實例的 `p` 讀取時,將調用 `Delegate` 中的 `getValue()` 函數,所以它第一個參數是讀出 `p` 的對象、第二個參數保存了對 `p` 自身的描述(例如你可以取它的名字)。 例如:
```kotlin
val e = Example()
println(e.p)
```
輸出結果:
```
Example@33a17727, thank you for delegating ‘p’ to me!
```
類似地,當我們給 `p` 賦值時,將調用 `setValue()` 函數。前兩個參數相同,第三個參數保存將要被賦予的值:
```kotlin
e.p = "NEW"
```
輸出結果:
```
NEW has been assigned to ‘p’ in Example@33a17727.
```
委托對象的要求規范可以在[下文](http://www.kotlincn.net/docs/reference/delegated-properties.html#%E5%B1%9E%E6%80%A7%E5%A7%94%E6%89%98%E8%A6%81%E6%B1%82)找到。
請注意,自 Kotlin 1.1 起你可以在函數或代碼塊中聲明一個委托屬性,因此它不一定是類的成員。你可以在下文找到[其示例](http://www.kotlincn.net/docs/reference/delegated-properties.html#%E5%B1%80%E9%83%A8%E5%A7%94%E6%89%98%E5%B1%9E%E6%80%A7%E8%87%AA-11-%E8%B5%B7)。
## 標準委托
Kotlin 標準庫為幾種有用的委托提供了工廠方法。
### 延遲屬性 Lazy
[`lazy()`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/lazy.html) 是接受一個 lambda 并返回一個 `Lazy <T>` 實例的函數,返回的實例可以作為實現延遲屬性的委托:第一次調用 `get()` 會執行已傳遞給 `lazy()` 的 lambda 表達式并記錄結果,后續調用 `get()` 只是返回記錄的結果。
```kotlin
val lazyValue: String by lazy {
println("computed!")
"Hello"
}
fun main() {
println(lazyValue)
println(lazyValue)
}
```
默認情況下,對于 lazy 屬性的求值是**同步鎖的(synchronized)**:該值只在一個線程中計算,并且所有線程會看到相同的值。如果初始化委托的同步鎖不是必需的,這樣多個線程可以同時執行,那么將`LazyThreadSafetyMode.PUBLICATION` 作為參數傳遞給 `lazy()` 函數。而如果你確定初始化將總是發生在與屬性使用位于相同的線程,那么可以使用 `LazyThreadSafetyMode.NONE` 模式:它不會有任何線程安全的保證以及相關的開銷。
### 可觀察屬性 Observable
[`Delegates.observable()`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.properties/-delegates/observable.html) 接受兩個參數:初始值與修改時處理程序(handler)。
每當我們給屬性賦值時會調用該處理程序(在賦值*后*執行)。它有三個參數:被賦值的屬性、舊值與新值:
```kotlin
import kotlin.properties.Delegates
class User {
var name: String by Delegates.observable("<no name>") {
prop, old, new ->
println("$old -> $new")
}
}
fun main() {
val user = User()
user.name = "first"
user.name = "second"
}
```
如果你想能夠截獲一個賦值并“否決”它,就使用 [`vetoable()`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.properties/-delegates/vetoable.html) 取代 `observable()`。
在屬性被賦新值生效*之前*會調用傳遞給 `vetoable` 的處理程序。
## 把屬性儲存在映射中
一個常見的用例是在一個映射(map)里存儲屬性的值。這經常出現在像解析 JSON 或者做其他“動態”事情的應用中。在這種情況下,你可以使用映射實例自身作為委托來實現委托屬性。
```kotlin
class User(val map: Map<String, Any?>) {
val name: String by map
val age: Int by map
}
```
在這個例子中,構造函數接受一個映射參數:
```kotlin
val user = User(mapOf(
"name" to "John Doe",
"age" to 25
))
```
委托屬性會從這個映射中取值(通過字符串鍵——屬性的名稱):
```kotlin
class User(val map: Map<String, Any?>) {
val name: String by map
val age: Int by map
}
fun main() {
val user = User(mapOf(
"name" to "John Doe",
"age" to 25
))
//sampleStart
println(user.name) // Prints "John Doe"
println(user.age) // Prints 25
//sampleEnd
}
```
這也適用于 *var*{:.keyword} 屬性,如果把只讀的 `Map` 換成 `MutableMap` 的話:
```kotlin
class MutableUser(val map: MutableMap<String, Any?>) {
var name: String by map
var age: Int by map
}
```
## 局部委托屬性(自 1.1 起)
你可以將局部變量聲明為委托屬性。例如,你可以使一個局部變量惰性初始化:
```kotlin
fun example(computeFoo: () -> Foo) {
val memoizedFoo by lazy(computeFoo)
if (someCondition && memoizedFoo.isValid()) {
memoizedFoo.doSomething()
}
}
```
`memoizedFoo` 變量只會在第一次訪問時計算。如果 `someCondition` 失敗,那么該變量根本不會計算。
## 屬性委托要求
這里我們總結了委托對象的要求。
對于一個**只讀**屬性(即 *val* 聲明的),委托必須提供一個名為 `getValue` 的函數,該函數接受以下參數:
* `thisRef` —— 必須與 _屬性所有者_ 類型(對于擴展屬性——指被擴展的類型)相同或者是它的超類型;
* `property` —— 必須是類型 `KProperty<*>` 或其超類型。
這個函數必須返回與屬性相同的類型(或其子類型)。
對于一個**可變**屬性(即 *var*{:.keyword} 聲明的),委托必須*額外*提供一個名為 `setValue` 的函數,該函數接受以下參數:
* `thisRef` —— 同 `getValue()`;
* `property` —— 同 `getValue()`;
* new value —— 必須與屬性同類型或者是它的子類型。
`getValue()` 或/與 `setValue()` 函數可以通過委托類的成員函數提供或者由擴展函數提供。當你需要委托屬性到原本未提供的這些函數的對象時后者會更便利。兩函數都需要用 `operator` 關鍵字來進行標記。
委托類可以實現包含所需 `operator` 方法的 `ReadOnlyProperty` 或 `ReadWriteProperty` 接口之一。這倆接口是在 Kotlin 標準庫中聲明的:
```kotlin
interface ReadOnlyProperty<in R, out T> {
operator fun getValue(thisRef: R, property: KProperty<*>): T
}
interface ReadWriteProperty<in R, T> {
operator fun getValue(thisRef: R, property: KProperty<*>): T
operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
}
```
### 翻譯規則
在每個委托屬性的實現的背后,Kotlin 編譯器都會生成輔助屬性并委托給它。
例如,對于屬性 `prop`,生成隱藏屬性 `prop$delegate`,而訪問器的代碼只是簡單地委托給這個附加屬性:
```kotlin
class C {
var prop: Type by MyDelegate()
}
// 這段是由編譯器生成的相應代碼:
class C {
private val prop$delegate = MyDelegate()
var prop: Type
get() = prop$delegate.getValue(this, this::prop)
set(value: Type) = prop$delegate.setValue(this, this::prop, value)
}
```
Kotlin 編譯器在參數中提供了關于 `prop` 的所有必要信息:第一個參數 `this` 引用到外部類 `C` 的實例而 `this::prop` 是 `KProperty` 類型的反射對象,該對象描述 `prop` 自身。
請注意,直接在代碼中引用[綁定的可調用引用](http://www.kotlincn.net/docs/reference/reflection.html#%E7%BB%91%E5%AE%9A%E7%9A%84%E5%87%BD%E6%95%B0%E4%B8%8E%E5%B1%9E%E6%80%A7%E5%BC%95%E7%94%A8%E8%87%AA-11-%E8%B5%B7)的語法 `this::prop` 自 Kotlin 1.1 起才可用。
### 提供委托(自 1.1 起)
通過定義 `provideDelegate` 操作符,可以擴展創建屬性實現所委托對象的邏輯。
如果 `by` 右側所使用的對象將 `provideDelegate` 定義為成員或擴展函數,那么會調用該函數來創建屬性委托實例。
`provideDelegate` 的一個可能的使用場景是在創建屬性時(而不僅在其 getter 或 setter 中)檢測屬性一致性。
例如,如果要在綁定之前檢測屬性名稱,可以這樣寫:
```kotlin
class ResourceDelegate<T> : ReadOnlyProperty<MyUI, T> {
override fun getValue(thisRef: MyUI, property: KProperty<*>): T { ... }
}
class ResourceLoader<T>(id: ResourceID<T>) {
operator fun provideDelegate(
thisRef: MyUI,
prop: KProperty<*>
): ReadOnlyProperty<MyUI, T> {
checkProperty(thisRef, prop.name)
// 創建委托
return ResourceDelegate()
}
private fun checkProperty(thisRef: MyUI, name: String) { …… }
}
class MyUI {
fun <T> bindResource(id: ResourceID<T>): ResourceLoader<T> { …… }
val image by bindResource(ResourceID.image_id)
val text by bindResource(ResourceID.text_id)
}
```
`provideDelegate` 的參數與 `getValue` 相同:
* `thisRef` —— 必須與 _屬性所有者_ 類型(對于擴展屬性——指被擴展的類型)相同或者是它的超類型;
* `property` —— 必須是類型 `KProperty<*>` 或其超類型。
在創建 `MyUI` 實例期間,為每個屬性調用 `provideDelegate` 方法,并立即執行必要的驗證。
如果沒有這種攔截屬性與其委托之間的綁定的能力,為了實現相同的功能,你必須顯式傳遞屬性名,這不是很方便:
```kotlin
// 檢測屬性名稱而不使用“provideDelegate”功能
class MyUI {
val image by bindResource(ResourceID.image_id, "image")
val text by bindResource(ResourceID.text_id, "text")
}
fun <T> MyUI.bindResource(
id: ResourceID<T>,
propertyName: String
): ReadOnlyProperty<MyUI, T> {
checkProperty(this, propertyName)
// 創建委托
}
```
在生成的代碼中,會調用 `provideDelegate` 方法來初始化輔助的 `prop$delegate` 屬性。
比較對于屬性聲明 `val prop: Type by MyDelegate()` 生成的代碼與[上面](http://www.kotlincn.net/docs/reference/delegated-properties.html#%E7%BF%BB%E8%AF%91%E8%A7%84%E5%88%99)(當 `provideDelegate` 方法不存在時)生成的代碼:
```kotlin
class C {
var prop: Type by MyDelegate()
}
// 這段代碼是當“provideDelegate”功能可用時
// 由編譯器生成的代碼:
class C {
// 調用“provideDelegate”來創建額外的“delegate”屬性
private val prop$delegate = MyDelegate().provideDelegate(this, this::prop)
var prop: Type
get() = prop$delegate.getValue(this, this::prop)
set(value: Type) = prop$delegate.setValue(this, this::prop, value)
}
```
請注意,`provideDelegate` 方法只影響輔助屬性的創建,并不會影響為 getter 或 setter 生成的代碼。
- 前言
- Kotlin簡介
- IntelliJ IDEA技巧總結
- idea設置類注釋和方法注釋模板
- 像Android Studion一樣創建工程
- Gradle
- Gradle入門
- Gradle進階
- 使用Gradle創建一個Kotlin工程
- 環境搭建
- Androidstudio平臺搭建
- Eclipse的Kotlin環境配置
- 使用IntelliJ IDEA
- Kotlin學習路線
- Kotlin官方中文版文檔教程
- 概述
- kotlin用于服務器端開發
- kotlin用于Android開發
- kotlin用于JavaScript開發
- kotlin用于原生開發
- Kotlin 用于數據科學
- 協程
- 多平臺
- 新特性
- 1.1的新特性
- 1.2的新特性
- 1.3的新特性
- 開始
- 基本語法
- 習慣用法
- 編碼規范
- 基礎
- 基本類型
- 包與導入
- 控制流
- 返回與跳轉
- 類與對象
- 類與繼承
- 屬性與字段
- 接口
- 可見性修飾符
- 擴展
- 數據類
- 密封類
- 泛型
- 嵌套類
- 枚舉類
- 對象
- 類型別名
- 內嵌類
- 委托
- 委托屬性
- 函數與Lambda表達式
- 函數
- Lambda表達式
- 內聯函數
- 集合
- 集合概述
- 構造集合
- 迭代器
- 區間與數列
- 序列
- 操作概述
- 轉換
- 過濾
- 加減操作符
- 分組
- 取集合的一部分
- 取單個元素
- 排序
- 聚合操作
- 集合寫操作
- List相關操作
- Set相關操作
- Map相關操作
- 多平臺程序設計
- 平臺相關聲明
- 以Gradle創建
- 更多語言結構
- 解構聲明
- 類型檢測與轉換
- This表達式
- 相等性
- 操作符重載
- 空安全
- 異常
- 注解
- 反射
- 作用域函數
- 類型安全的構造器
- Opt-in Requirements
- 核心庫
- 標準庫
- kotlin.test
- 參考
- 關鍵字與操作符
- 語法
- 編碼風格約定
- Java互操作
- Kotlin中調用Java
- Java中調用Kotlin
- JavaScript
- 動態類型
- kotlin中調用JavaScript
- JavaScript中調用kotlin
- JavaScript模塊
- JavaScript反射
- JavaScript DCE
- 原生
- 并發
- 不可變性
- kotlin庫
- 平臺庫
- 與C語言互操作
- 與Object-C及Swift互操作
- CocoaPods集成
- Gradle插件
- 調試
- FAQ
- 協程
- 協程指南
- 基礎
- 取消與超時
- 組合掛起函數
- 協程上下文與調度器
- 異步流
- 通道
- 異常處理與監督
- 共享的可變狀態與并發
- Select表達式(實驗性)
- 工具
- 編寫kotlin代碼文檔
- 使用Kapt
- 使用Gradle
- 使用Maven
- 使用Ant
- Kotlin與OSGI
- 編譯器插件
- 編碼規范
- 演進
- kotlin語言演進
- 不同組件的穩定性
- kotlin1.3的兼容性指南
- 常見問題
- FAQ
- 與Java比較
- 與Scala比較(官方已刪除)
- Google開發者官網簡介
- Kotlin and Android
- Get Started with Kotlin on Android
- Kotlin on Android FAQ
- Android KTX
- Resources to Learn Kotlin
- Kotlin樣品
- Kotlin零基礎到進階
- 第一階段興趣入門
- kotlin簡介和學習方法
- 數據類型和類型系統
- 入門
- 分類
- val和var
- 二進制基礎
- 基礎
- 基本語法
- 包
- 示例
- 編碼規范
- 代碼注釋
- 異常
- 根類型“Any”
- Any? 可空類型
- 可空性的實現原理
- kotlin.Unit類型
- kotlin.Nothing類型
- 基本數據類型
- 數值類型
- 布爾類型
- 字符型
- 位運算符
- 變量和常量
- 語法和運算符
- 關鍵字
- 硬關鍵字
- 軟關鍵字
- 修飾符關鍵字
- 特殊標識符
- 操作符和特殊符號
- 算術運算符
- 賦值運算符
- 比較運算符
- 邏輯運算符
- this關鍵字
- super關鍵字
- 操作符重載
- 一元操作符
- 二元操作符
- 字符串
- 字符串介紹和屬性
- 字符串常見方法操作
- 字符串模板
- 數組
- 數組介紹創建及遍歷
- 數組常見方法和屬性
- 數組變化以及下標越界問題
- 原生數組類型
- 區間
- 正向區間
- 逆向區間
- 步長
- 類型檢測與類型轉換
- is、!is、as、as-運算符
- 空安全
- 可空類型變量
- 安全調用符
- 非空斷言
- Elvis操作符
- 可空性深入
- 可空性和Java
- 函數
- 函數式編程概述
- OOP和FOP
- 函數式編程基本特性
- 組合與范疇
- 在Kotlin中使用函數式編程
- 函數入門
- 函數作用域
- 函數加強
- 命名參數
- 默認參數
- 可變參數
- 表達式函數體
- 頂層、嵌套、中綴函數
- 尾遞歸函數優化
- 函數重載
- 控制流
- if表達式
- when表達式
- for循環
- while循環
- 循環中的 Break 與 continue
- return返回
- 標簽處返回
- 集合
- list集合
- list集合介紹和操作
- list常見方法和屬性
- list集合變化和下標越界
- set集合
- set集合介紹和常見操作
- set集合常見方法和屬性
- set集合變換和下標越界
- map集合
- map集合介紹和常見操作
- map集合常見方法和屬性
- map集合變換
- 集合的函數式API
- map函數
- filter函數
- “ all ”“ any ”“ count ”和“ find ”:對集合應用判斷式
- 別樣的求和方式:sumBy、sum、fold、reduce
- 根據人的性別進行分組:groupBy
- 扁平化——處理嵌套集合:flatMap、flatten
- 惰性集合操作:序列
- 區間、數組、集合之間轉換
- 面向對象
- 面向對象-封裝
- 類的創建及屬性方法訪問
- 類屬性和字段
- 構造器
- 嵌套類(內部類)
- 枚舉類
- 枚舉類遍歷&枚舉常量常用屬性
- 數據類
- 密封類
- 印章類(密封類)
- 面向對象-繼承
- 類的繼承
- 面向對象-多態
- 抽象類
- 接口
- 接口和抽象類的區別
- 面向對象-深入
- 擴展
- 擴展:為別的類添加方法、屬性
- Android中的擴展應用
- 優化Snackbar
- 用擴展函數封裝Utils
- 解決煩人的findViewById
- 擴展不是萬能的
- 調度方式對擴展函數的影響
- 被濫用的擴展函數
- 委托
- 委托類
- 委托屬性
- Kotlin5大內置委托
- Kotlin-Object關鍵字
- 單例模式
- 匿名類對象
- 伴生對象
- 作用域函數
- let函數
- run函數
- with函數
- apply函數
- also函數
- 標準庫函數
- takeIf 與 takeUnless
- 第二階段重點深入
- Lambda編程
- Lambda成員引用高階函數
- 高階函數
- 內聯函數
- 泛型
- 泛型的分類
- 泛型約束
- 子類和子類型
- 協變與逆變
- 泛型擦除與實化類型
- 泛型類型參數
- 泛型的背后:類型擦除
- Java為什么無法聲明一個泛型數組
- 向后兼容的罪
- 類型擦除的矛盾
- 使用內聯函數獲取泛型
- 打破泛型不變
- 一個支持協變的List
- 一個支持逆變的Comparator
- 協變和逆變
- 第三階段難點突破
- 注解和反射
- 聲明并應用注解
- DSL
- 協程
- 協程簡介
- 協程的基本操作
- 協程取消
- 管道
- 慕課霍丙乾協程筆記
- Kotlin與Java互操作
- 在Kotlin中調用Java
- 在Java中調用Kotlin
- Kotlin與Java中的操作對比
- 第四階段專題練習
- 朱凱Kotlin知識點總結
- Kotlin 基礎
- Kotlin 的變量、函數和類型
- Kotlin 里那些「不是那么寫的」
- Kotlin 里那些「更方便的」
- Kotlin 進階
- Kotlin 的泛型
- Kotlin 的高階函數、匿名函數和 Lambda 表達式
- Kotlin協程
- 初識
- 進階
- 深入
- Kotlin 擴展
- 會寫「18.dp」只是個入門——Kotlin 的擴展函數和擴展屬性(Extension Functions / Properties)
- Kotlin實戰-開發Android