委托 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">-&gt;</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">-&gt;</span>
        <span class="n">newValue</span> <span class="p">&gt;</span> <span class="mi">0</span> <span class="p">&amp;&amp;</span> <span class="n">newValue</span> <span class="p">&lt;</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。

這用法最常見於:

  • 全域設定
  • 多類別共享資料
  • 全域狀態管理

results matching ""

    No results matching ""