Kotlin 委譲

委譲パターンはソフトウェア設計パターンにおける基本的なテクニックの一つです。委譲パターンでは、2つのオブジェクトが同じリクエストの処理に関与し、リクエストを受け取ったオブジェクトはリクエストを別のオブジェクトに委譲して処理します。

Kotlin は委譲パターンを直接サポートしており、より優雅で簡潔です。Kotlin はキーワード by によって委譲を実現します。


クラス委譲

クラスの委譲とは、あるクラスに定義されたメソッドが、実際には別のクラスのオブジェクトのメソッドを呼び出すことによって実装されることです。

次の例では、派生クラス Derived はインターフェース Base のすべてのメソッドを継承し、渡された Base クラスのオブジェクトに委譲してこれらのメソッドを実行します。

// 创建接口
interface Base {   
    fun print()
}

// 实现此接口的被委托的类
class BaseImpl(val x: Int) : Base {
    override fun print() { print(x) }
}

// 通过关键字 by 建立委托类
class Derived(b: Base) : Base by b

fun main(args: Array<String>) {
    val b = BaseImpl(10)
    Derived(b).print() // 输出 10
}

Derived の宣言では、by 句は b を Derived のオブジェクトインスタンス内に保存することを示し、コンパイラは Base インターフェースから継承したすべてのメソッドを生成して、呼び出しを b に転送します。


プロパティ委譲

プロパティ委譲とは、クラスのあるプロパティの値をクラス内で直接定義するのではなく、それをプロキシクラスに委ねることで、そのクラスのプロパティを統一的に管理することを指します。

プロパティ委譲の構文形式:

val/var <属性名>: <类型> by <表达式>
  • var/val:プロパティの型(可変/読み取り専用)
  • プロパティ名:プロパティの名前
  • 型:プロパティのデータ型
  • 式:委譲プロキシクラス

by キーワードの後の式が委譲です。プロパティの get() メソッド(および set() メソッド)は、このオブジェクトの getValue() および setValue() メソッドに委譲されます。プロパティ委譲はインターフェースを実装する必要はありませんが、getValue() 関数(var プロパティの場合は setValue() 関数も)を提供する必要があります。

委譲先のクラスを定義する

このクラスには getValue() メソッドと setValue() メソッドを含める必要があります。また、パラメータ thisRef は委譲を行うクラスのオブジェクト、prop は委譲を行うプロパティのオブジェクトです。

import kotlin.reflect.KProperty
// 定义包含属性委托的类
class Example {
    var p: String by Delegate()
}

// 委托的类
class Delegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, 这里委托了 ${property.name} 属性"
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$thisRef 的 ${property.name} 属性赋值为 $value")
    }
}
fun main(args: Array<String>) {
    val e = Example()
    println(e.p)     // 访问该属性,调用 getValue() 函数

    e.p = "サンプル"   // 调用 setValue() 函数
    println(e.p)
}

実行結果は次のとおりです:

Example@433c675d, 这里委托了 p 属性
Example@433c675d 的 p 属性赋值为 サンプル
Example@433c675d, 这里委托了 p 属性

標準委譲

Kotlin の標準ライブラリには、プロパティの委譲を実装するためのファクトリメソッドが多数組み込まれています。

遅延プロパティ Lazy

lazy() は関数で、Lambda 式をパラメータとして受け取り、Lazy <T> インスタンスを返す関数です。返されるインスタンスは遅延プロパティを実装するための委譲として使用できます。最初の get() 呼び出しでは lazy() に渡されたラムダ式を実行して結果を記録し、その後の get() 呼び出しでは記録された結果を返すだけです。

val lazyValue: String by lazy {
    println("computed!")     // 第一次调用输出,第二次调用不执行
    "Hello"
}

fun main(args: Array<String>) {
    println(lazyValue)   // 第一次执行,执行两次输出表达式
    println(lazyValue)   // 第二次执行,只输出返回值
}

実行出力結果:

computed!
Hello
Hello

監視可能プロパティ Observable

observable はオブザーバーパターンの実装に使用できます。

Delegates.observable() 関数は2つのパラメータを受け取ります。1つ目は初期化値、2つ目はプロパティ値変更イベントのレスポンダー(ハンドラー)です。

プロパティへの代入後にイベントのレスポンダー(ハンドラー)が実行されます。このハンドラーには、代入されたプロパティ、旧値、新値の3つのパラメータがあります:

import kotlin.properties.Delegates

class User {
    var name: String by Delegates.observable("初始值") {
        prop, old, new ->
        println("旧值:$old -> 新值:$new")
    }
}

fun main(args: Array<String>) {
    val user = User()
    user.name = "第一次赋值"
    user.name = "第二次赋值"
}

実行出力結果:

旧値:初期値 → 新値:最初の代入
旧値:最初の代入 → 新値:2回目の代入

プロパティをマップに格納する

一般的なユースケースとして、プロパティの値をマップ(map)に格納することがあります。これはJSONの解析やその他の「動的な」処理を行うアプリケーションでよく見られます。この場合、マップインスタンス自体を委譲として使用して委譲プロパティを実装できます。

class Site(val map: Map<String, Any?>) {
    val name: String by map
    val url: String  by map
}

fun main(args: Array<String>) {
    // 构造函数接受一个映射参数
    val site = Site(mapOf(
        "name" to "サンプル",
        "url"  to "www.example.com"
    ))
    
    // 读取映射值
    println(site.name)
    println(site.url)
}

実行出力結果:

サンプル
www.example.com

var プロパティを使用する場合は、Map を MutableMap に置き換える必要があります:

class Site(val map: MutableMap<String, Any?>) {
    val name: String by map
    val url: String by map
}

fun main(args: Array<String>) {

    var map:MutableMap<String, Any?> = mutableMapOf(
            "name" to "サンプル",
            "url" to "www.example.com"
    )

    val site = Site(map)

    println(site.name)
    println(site.url)

    println("--------------")
    map.put("name", "Google")
    map.put("url", "www.google.com")

    println(site.name)
    println(site.url)

}

実行出力結果:

サンプル
www.example.com
--------------
Google
www.google.com

Not Null

notNull は、初期化段階でプロパティの値を決定できない場合に適しています。

class Foo {
    var notNullBar: String by Delegates.notNull<String>()
}

foo.notNullBar = "bar"
println(foo.notNullBar)

プロパティが代入前にアクセスされると例外がスローされることに注意してください。


ローカル委譲プロパティ

ローカル変数を委譲プロパティとして宣言できます。例えば、ローカル変数を遅延初期化できます:

fun example(computeFoo: () -> Foo) {
    val memoizedFoo by lazy(computeFoo)

    if (someCondition && memoizedFoo.isValid()) {
        memoizedFoo.doSomething()
    }
}

memoizedFoo 変数は最初にアクセスされたときにのみ計算されます。someCondition が失敗した場合、その変数はまったく計算されません。


プロパティ委譲の要件

読み取り専用プロパティ(つまり val プロパティ)の場合、その委譲は getValue() という名前の関数を提供する必要があります。この関数は次のパラメータを受け取ります:

  • thisRef —— プロパティ所有者の型(拡張プロパティの場合は拡張される型)と同じか、そのスーパータイプである必要があります
  • property —— 型 KProperty<*> またはそのスーパータイプである必要があります

この関数はプロパティと同じ型(またはそのサブタイプ)を返す必要があります。

値が可変(mutable)のプロパティ(つまり var プロパティ)の場合、getValue() 関数に加えて、その委譲はさらに setValue() という名前の関数を提供する必要があります。この関数は次のパラメータを受け取ります:

  • property —— 型 KProperty<*> またはそのスーパータイプである必要があります
  • new value —— プロパティと同じ型またはそのスーパータイプである必要があります。

翻訳規則

各委譲プロパティの実装の背後では、Kotlin コンパイラが補助プロパティを生成し、それに委譲します。例えば、プロパティ prop の場合、隠しプロパティ prop$delegate が生成され、アクセサのコードは単にこの追加プロパティに委譲します:

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 自体を説明します。


委譲の提供

provideDelegate 演算子を定義することで、プロパティの実装に使用する委譲オブジェクトを作成するロジックを拡張できます。by の右側で使用されるオブジェクトが provideDelegate をメンバー関数または拡張関数として定義している場合、その関数が呼び出されてプロパティ委譲インスタンスが作成されます。

provideDelegate の可能な使用シナリオの1つは、プロパティの作成時(getter や setter だけでなく)にプロパティの一貫性をチェックすることです。

例えば、バインドする前にプロパティ名をチェックしたい場合は、次のように書くことができます:

class ResourceLoader<T>(id: ResourceID<T>) {
    operator fun provideDelegate(
            thisRef: MyUI,
            prop: KProperty<*>
    ): ReadOnlyProperty<MyUI, T> {
        checkProperty(thisRef, prop.name)
        // 创建委托
    }

    private fun checkProperty(thisRef: MyUI, name: String) { …… }
}

fun <T> bindResource(id: ResourceID<T>): ResourceLoader<T> { …… }

class MyUI {
    val image by bindResource(ResourceID.image_id)
    val text by bindResource(ResourceID.text_id)
}

provideDelegate のパラメータは getValue と同じです:

  • thisRef —— プロパティ所有者の型(拡張プロパティの場合は拡張される型)と同じか、そのスーパータイプである必要があります
  • property —— 型 KProperty<*> またはそのスーパータイプである必要があります。

MyUI インスタンスの作成中に、各プロパティに対して provideDelegate メソッドが呼び出され、必要な検証が直ちに実行されます。

このようなプロパティとその委譲との間のバインディングをインターセプトする機能がなければ、同じ機能を実現するためにプロパティ名を明示的に渡す必要があり、あまり便利ではありません:

// 检查属性名称而不使用“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() に対して生成されたコードと、上記(provideDelegate メソッドが存在しない場合)に生成されたコードを比較してください:

class C {
    var prop: Type by MyDelegate()
}

// 这段代码是当“provideDelegate”功能可用时
// 由编译器生成的代码:
class C {
    // 调用“provideDelegate”来创建额外的“delegate”属性
    private val prop$delegate = MyDelegate().provideDelegate(this, this::prop)
    val prop: Type
        get() = prop$delegate.getValue(this, this::prop)
}

provideDelegate メソッドは補助プロパティの作成にのみ影響し、getter や setter のために生成されるコードには影響しないことに注意してください。

その他の拡張