委托 by Delegation
類別委托
Kotlin的委托,我覺得十分像設計模式中的代理人模式。
就是旅客跟旅行社買機票,但實際付錢是旅客付。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// BuyTicket介面 要做的事
interface BuyTicket {
fun pay()
}
// 旅客實作BuyTicket介面的pay方法。
// 真正要做事的人
class Tourist : BuyTicket {
override fun pay() = println("旅客提供信用卡 卡號付錢")
}
// 旅行社實作BuyTicket介面的pay方法,實際上是旅客付錢。
// 代理人
class Agency(tourist: BuyTicket) : BuyTicket by tourist
1
2
3
4
5
6
7
8
fun main() {
// 真正要做事的人
val tourist = Tourist()
// 代理人,把 真正要做事的人 的傳入
val agency = Agency(tourist)
// 實際上是旅客付錢,不是代理人付錢
agency.pay()
}
旅客提供信用卡 卡號付錢
以上的內容要有Java程式碼對映才比較明白。
BuyTicket介面
1
2
3
public interface BuyTicket {
void pay();
}
旅客實作BuyTicket介面的pay方法。
1
2
3
4
5
6
public class Tourist implements BuyTicket{
@Override
public void pay() {
System.out.println("旅客提供信用卡 卡號付錢");
}
}
旅行社實作BuyTicket介面的pay方法,實際上是旅客付錢。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Agency implements BuyTicket{
// 成員屬性要有旅客Tourist
private Tourist tourist;
// 使用建構子參數,把旅客傳入旅行社
public Agency(Tourist tourist) {
this.tourist = tourist;
}
@Override
public void pay() {
// 實際上是呼叫旅客的pay()方法
tourist.pay();
}
}
Client測試
1
2
3
4
5
6
7
8
9
10
public class Client {
public static void main(String[] args) {
// 建立旅客
Tourist tourist = new Tourist();
// 把旅客傳入旅行社的建構子
Agency agency = new Agency(tourist);
// 旅行社付錢,實際上旅客付的。
agency.pay();
}
}
旅客提供信用卡 卡號付錢
語法
class 代理人(真正要做事的人: 要做的事) : 要做的事 by 真正要做事的人
class Agency(tourist: BuyTicket) : BuyTicket by tourist
屬性委托
要先了解operator是什麼才能繼續往下。
Kotlin 的 by 委託,是用來把「屬性的 getter / setter 行為」交給另一個物件處理。
語法
var 屬性名 by 委托類別
var address by someDelegate
使用 by 委托關鍵字,委托的類別要實作setValue()與getValue()方法。
- getValue:當你讀取屬性時會被呼叫
- setValue:當你寫入屬性時會被呼叫(只針對 var,val 沒有 setValue)
getValue
operator fun getValue(thisRef: Any?, property: KProperty<*>): T
- thisRef 擁有這個屬性的物件實例(如果是 companion object 就是類別)
- property 屬性本身的描述資訊,例如 name 或 age
- 回傳值 屬性的值
setValue
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T)
- thisRef 擁有這個屬性的物件實例
- property 屬性描述資訊
- value 新的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import kotlin.reflect.KProperty
class Student {
var address: String by AddressDelegate()
}
class AddressDelegate {
// 需要有一個暫存變數temp,儲存變數的值
// 變數預設值為no data
private var temp = "no data"
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
println("$thisRef ,讀取屬性 ${property.name}")
// get()的時候,傳回暫存變數
return temp
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String){
// 設定暫存變數temp
temp = value
println("$thisRef , 設定屬性 ${property.name} change to $value")
}
}
fun main() {
val student = Student()
// call AddressDelegate getValue()
println(student.address)
// call AddressDelegate setValue()
student.address = "Taiwan"
// call AddressDelegate getValue()
println(student.address)
}
no data
learn2.Student@4783da3f , 設定屬性 address change to Taiwan
learn2.Student@4783da3f , 讀取屬性 address
Taiwan
thisRef
thisRef 與 property 讓 delegate 可以知道「屬性屬於哪個物件」以及「屬性名稱」
getValue setValue
- operator by
語法
by 委托物件委托的物件,必須實作以下二個屬性。 ``` operator fun getValue → 當你讀取屬性時呼叫
operator fun setValue → 當你寫入屬性時呼叫
by 就是把屬性的「存取行為」委託給這兩個函式
## KProperty
- [kotlin反射][6]
`KProperty<*>` 告訴你的 delegate:「你正在操作的屬性叫什麼名字、型別是什麼」
property.name 屬性的名稱
property.returnType 屬性的類型
## Kotlin 標準庫內建的委託
### by Delegates.observable() 屬性變化監聽
可以監聽屬性值改變時的事件。<br>
語法<br>
var name:String by Delegates.observable(“初始值”) { property, oldValue, newValue -> println(“${property.name} from $oldValue change to $newValue”) }
<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="code"><pre><span class="k">import</span> <span class="nn">kotlin.properties.Delegates</span>
<span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
<span class="kd">var</span> <span class="py">name</span><span class="p">:</span><span class="nc">String</span> <span class="k">by</span> <span class="nc">Delegates</span><span class="p">.</span><span class="nf">observable</span><span class="p">(</span><span class="s">"empty"</span><span class="p">)</span> <span class="p">{</span>
<span class="n">property</span><span class="p">,</span> <span class="n">oldValue</span><span class="p">,</span> <span class="n">newValue</span> <span class="p">-></span>
<span class="nf">println</span><span class="p">(</span><span class="s">"${property.name} from $oldValue change to $newValue"</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">name</span> <span class="p">=</span> <span class="s">"Hello"</span>
<span class="n">name</span> <span class="p">=</span> <span class="s">"World"</span>
</pre></td></tr></tbody></table></code></pre></figure>
name from empty change to Hello name from Hello change to World
常用於 UI binding 或 資料同步 場景,比如 ViewModel 中監控狀態變化。
### by Delegates.vetoable() 可拒絕的屬性變更
比 observable 多一步「審核機制」,可以決定是否接受新值。<br>
以下newValue必須大於0,小於150,才可以設定新的值。<br>
語法<br>
var age:Int by Delegates.vetoable(初始值) { property, oldValue, newValue -> newValue > 0 && newValue < 150 }
<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="code"><pre><span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
<span class="kd">var</span> <span class="py">age</span><span class="p">:</span><span class="nc">Int</span> <span class="k">by</span> <span class="nc">Delegates</span><span class="p">.</span><span class="nf">vetoable</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
<span class="n">property</span><span class="p">,</span> <span class="n">oldValue</span><span class="p">,</span> <span class="n">newValue</span> <span class="p">-></span>
<span class="n">newValue</span> <span class="p">></span> <span class="mi">0</span> <span class="p">&&</span> <span class="n">newValue</span> <span class="p"><</span> <span class="mi">150</span>
<span class="p">}</span>
<span class="n">age</span> <span class="p">=</span> <span class="mi">10</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"age = $age"</span><span class="p">)</span>
<span class="n">age</span> <span class="p">=</span> <span class="p">-</span><span class="mi">1</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"age = $age"</span><span class="p">)</span>
<span class="n">age</span> <span class="p">=</span> <span class="mi">200</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"age = $age"</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>
age = 10 age = 10 age = 10 ``` 適合用在「狀態限制」或「驗證條件」場景,例如不能設定負值、非法輸入等。
by 與 二個冒號::的引用
全域變數
1
2
3
4
5
var topLevelValue: String = "初始值"
class Person {
var name: String by ::topLevelValue
}
::topLevelValue → 取得「全域變數 topLevelValue 的屬性引用(KProperty)」
by ::topLevelValue → 將 name 的 getter/setter 委託給這個屬性
成員變數
1
2
3
4
class User {
private var realValue: String = "default"
var name: String by this::realValue
}
this::realValue → 取得「本 class 的成員 realValue 的屬性引用」
用在 by 上,讓 name 的讀寫委託給 realValue
其它類別成員
1
2
3
4
5
6
7
class Storage {
var data: String = "init"
}
class Article(val storage: Storage) {
var title: String by storage::data
}
storage::data → 取得 storage 物件的 data 成員的引用
Article.title 的 getter/setter 會交給 storage.data
by與 兩個冒號::使用時機
做「資料別名」(alias)或「映射」
有時你想讓 class 的屬性只是「另一個地方的值的別名」。
例如:
1
2
3
4
5
var coreName: String = "SystemCore"
class AppInfo {
var name: String by ::coreName
}
等於 AppInfo.name 指向 coreName。 像「參考 / alias」,但用 Kotlin 語法實現。
想把資料集中管理,而不是分散在每個 class
有時候一筆資料應該放在「全域」,但希望 class 裡面用起來像自己的屬性。
例:
1
2
3
4
5
var globalSetting: String = "default"
class Settings {
var theme: String by ::globalSetting
}
使用端:
1
2
val s = Settings()
println(s.theme)
看起來像:
s.theme
但本質是:
globalSetting
多個類別想共用同一筆資料
如果多個 class 都要共享一個值,例如:
全域狀態
設定值(App config)
使用者登入資訊
全域計數器
你可以把該數據放在全域變數,再讓多個物件用委託方式共用它。
範例
1
2
3
4
5
6
7
var globalToken: String = "init"
class A {
var token: String by ::globalToken
}
class B {
var token: String by ::globalToken
A().token = “123” 會直接改到 B 的 token。
這用法最常見於:
- 全域設定
- 多類別共享資料
- 全域狀態管理