しめ鯖日記

swift, iPhoneアプリ開発, ruby on rails等のTipsや入門記事書いてます

Kotlinの委譲用のキーワードのbyを試してみる

Kotlinのbyというキーワードが分からなかったので調べてみました。

byを使ってみる

byとは委譲に使うキーワードです。
実際に動かしながら委譲やbyについて見ていきます。

まずはInterfaceとInterfaceを適用したクラスを作成します。

interface TestInterface {
    val property: String
    fun method(): Int
}

class MyClass1: TestInterface {
    override val property = "1"
    override fun method(): Int { return 1 }
}

次に同じInterfaceを適用したクラスを作成します。
このクラスは初期化時にMyClass1を受け取り、内部ではMyClass1のメソッドやプロパティーを呼び出します。

class MyClass2(private val myClass1: MyClass1): TestInterface {
    override val property = myClass1.property
    override fun method(): Int { return myClass1.method() }
}

実際に動かしてみるとMyClass1のプロパティーが呼ばれている事が分かります。
このように外部のクラスのプロパティーやメソッドを利用する事を委譲といいます。
byというキーワードは委譲を簡単にするキーワードです。

Log.d("Test", "${MyClass2(MyClass1()).property}") // → 1

byを使ってMyClass2を書き直すと下のようになります。
これでTestInterfaceに関する処理はすべてmyClass1のものを呼び出すようになります。

class MyClass2(private val myClass1: MyClass1): TestInterface by myClass1

下のように一部の処理だけは委譲しない事も可能です。

class MyClass2(private val myClass1: MyClass1): TestInterface by myClass1 {
    override val property = "2"
}

Log.d("Test", "${MyClass2(MyClass1()).property}") // → 2

複数のInterfaceを適用して委譲する場合はそれぞれにbyを付ける必要があります。

interface TestInterface {
    val property: String
    fun method(): Int
}

interface TestInterface2 {
    val property2: String
}

class MyClass1: TestInterface, TestInterface2 {
    override val property = "1"
    override fun method(): Int { return 1 }
    override val property2 = "property2"
}

class MyClass2(private val myClass1: MyClass1): TestInterface by myClass1, TestInterface2 by myClass1

下のようにインスタンスとInterfaceの型があってない場合はエラーになります。
もしTestInterface2用のプロパティーを実装してあってもエラーです。

class MyClass1: TestInterface {
    override val property = "1"
    override fun method(): Int { return 1 }
    val property2 = "property2"
}

class MyClass2(private val myClass1: MyClass1): TestInterface2 by myClass1 // → Type mismatch.エラー

下のように委譲先(MyClass1)のプロパティーの値を変えるとMyClass2のproperty呼び出し時の値も変わります。

interface TestInterface {
    val property: String
}

class MyClass1: TestInterface {
    override var property = "1"
}

class MyClass2(private val myClass1: MyClass1): TestInterface by myClass1

val myClass1 = MyClass1()
Log.d("Test", "${MyClass2(myClass1).property}") // → 1
myClass1.property = "2"
Log.d("Test", "${MyClass2(myClass1).property}") // → 2

委譲プロパティについて

上記ではクラスに対してbyを使いましたがプロパティーに使う事もできます。
具体的な使い方は下の通りです。
myPropertyのgetterとsettterをDelegateClassに委譲しています。

Log.d("Test", "${MyClass().myProperty}") // → 1
MyClass().myProperty = "2"

class DelegateClass {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "1"
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        Log.d("Test", "$thisRef") // → com.example.myapp.MyClass@4dea06e
        Log.d("Test", "$value") // → 2
        Log.d("Test", "${property.name}") // → myProperty
    }
}

class MyClass {
    var myProperty: String by DelegateClass()
}

同じDelegateインスタンスを使い回す事で、下のように値を複数クラスで共有する事もできます。

class DelegateClass {
    var myProperty = ""

    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return myProperty
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        myProperty = value
    }
}

class MyClass(delegate: DelegateClass) {
    var myProperty: String by delegate
}

val delegate = DelegateClass()
Log.d("Test", "${MyClass(delegate).myProperty}") // → ""
MyClass(delegate).myProperty = "2"
Log.d("Test", "${MyClass(delegate).myProperty}") // → 2

Mapとbyについて

byを使うとMapの中の値にアクセスする事も可能です。
具体的には下の通りです、JSONデータの展開などで活躍しそうです。

class MyClass(delegate: Map<String, String>) {
    val myProperty: String by delegate
}

Log.d("Test", "${MyClass(mapOf("myProperty" to "1")).myProperty}")

Mapに該当のキーがない場合、NoSuchElementExceptionエラーになります。

class MyClass(delegate: Map<String, String>) {
    val myProperty: String by delegate
}

Log.d("Test", "${MyClass(mapOf("myProperty2" to "1")).myProperty}")

MutableMapを使う事で値のセットもできるようになります。

class MyClass(delegate: MutableMap<String, String>) {
    var myProperty: String by delegate
}

val map = mutableMapOf("myProperty" to "1")
Log.d("Test", "${MyClass(map).myProperty}") // → 1
MyClass(map).myProperty = "2"
Log.d("Test", "${MyClass(map).myProperty}") // → 2