English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

kotlin 委任

デリゲートパターンはソフトウェア設計パターンの基本技術の一つです。デリゲートパターンでは、同じリクエストを処理するために二つのオブジェクトが関与し、リクエストを受け取ったオブジェクトがリクエストを別のオブジェクトにデリゲートします。

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()関数を提供する必要があります(可変属性の場合、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 = "w3codebox"   // setValue()関数を呼び出します
    println(e.p)
}

出力結果は以下の通りです:

Example@433c675d,ここでp属性をデリゲートにしています
Example@433c675dのp属性をwに設定します3codebox
Example@433c675d,ここでp属性をデリゲートにしています

標準デリゲート

Kotlinの標準ライブラリには、属性のデリゲートを実現するための多くのファクトリメソッドが内蔵されています。

遅延属性 Lazy

lazy()は関数で、Lambda式をパラメータとして受け取り、Lazy<T>インスタンスを返す関数です。返されたインスタンスは、遅延属性のデリゲートとして使用できます:第一次のget()呼び出しでは、lazy()に渡されたLambda式が実行され、結果が記録されます。以降のget()呼び出しでは、記録された結果のみが返されます。

val lazyValue: String by lazy {
    println("computed!")     // 第一次呼び出し出力、第二次呼び出しは実行されません
    "Hello"
}
fun main(args: Array<String>) {
    println(lazyValue)   // 第一次実行、2回出力
    println(lazyValue)   // 第二次実行、返り値のみ出力
}

実行結果:

computed!
Hello
Hello

観察可能な属性 Observable

observableはオブザーバーモードを実現するために使用できます。

Delegates.observable() 函数は2つのパラメータを受け取ります: 最初のパラメータは初期値、2番目のパラメータは属性値の変更イベントのリスナー(handler)です。

属性の代入後にイベントリスナー(handler)が実行されます。これは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 = "第二次代入"
}

実行結果:

旧値:初期値 -> 新値:第一次代入
旧値:第一次代入 -> 新値:第二次代入

属性をマップに保存します

一般的な用例として、属性の値をマップ(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.w"}}3codebox.com"
    ))
    
    // マッピング値の読み取り
    println(site.name)
    println(site.url)
}

実行結果:

基礎チュートリアルウェブ
ja.oldtoolbag.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 "ja.oldtoolbag.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)
}

実行結果:

基礎チュートリアルウェブ
ja.oldtoolbag.com
--------------
Google
www.google.com

ノットヌル

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またはその超タイプ

この関数は、属性と同じタイプ(またはそのサブタイプ)を返す必要があります。

値が変更可能(可変)な属性(つまりvar属性)の場合、getValue()関数の他に、setValue()と呼ばれる別の関数も提供する必要があります。この関数は以下の引数を受け取ります:

  • thisRef —— 必須として属性の所有者タイプ(拡張属性の場合、拡張されるタイプ)と同じまたはその超タイプ

  • 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の使用例の一つは、属性を作成する際に(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 が生成するコードには影響を与えないことです。