Kotlin Delegate Property について調べてみた

# はじめに

KotlinはよくJavaと比較されて話されることがありますが、Kotlinにあり、Javaにはない言語機能が多数あります。その中の一つに delegate property というものがあり調べてみました。

# delegate property とは

delegate property とは名前の通り、「委譲されたプロパティ」です。もう少しわかり易い言葉で説明すると、変数のゲッター・セッターの処理を本来とは別のクラスに任せる事ができるわけです。処理が委譲できるとボイラーテンプレートや遅延評価などが可能になります。

使い方はとても簡単です。まず、委譲するクラスを実装し `by` キーワードで変数を宣言するだけです。以下に例を示します。

``` kotlin
class Example {
    var p: String by Delegate()
}
```

`by` キーワードで `Delegate` クラスをインスタンス化し指定しました。`Delegate` クラスは以下のようになっています。

``` kotlin
// Kotlin のリフレクションを使用
import kotlin.reflect.KProperty

class Delegate {

    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, delegate ${property.name}"
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$thisRef, delegate ${property.name}, value is $value")
    }

}
```

Kotlin-REPL で以上を実行してみました。結果は以下のようになります。

``` kotlin
>>> val example = Example()
>>> println(example.p)
Line_66$Example@3c3b70b7, delegate p
>>> example.p = "Hello, I am new value"
Line_66$Example@3c3b70b7, delegate p, value is Hello, I am new value
```

`Example` クラスのフィールド `p` のゲッター・セッターの処理を `Delegate` クラスに移乗できているのがわかります。面白いのが委譲先のクラスで、委譲されているプロパティの情報を入手できるところです。情報を入手できればそのプロパティがどういった名前で宣言されているかなどを元に、処理を記述することができてなにかオモシロそうな事ができるような気がします！

delegate property を利用することで遅延評価などを実装できるのですが、よく使われるであろう機能については標準ライブラリですでに実装されています。その中でも代表的な `lazy` と `observable` を以下に示します。

# delegate property による遅延評価

[`lazy()`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/lazy.html) 関数を利用することで遅延評価を実現できます。REPLでやってみた結果を以下に示します。

``` kotlin
>>> val lazyValue: String by lazy {
...     println("computed!")
...     "Hello"
... }
>>> println(lazyValue)
computed!
Hello
>>> println(lazyValue)
Hello
>>>
```

最初の呼び出しでは `computed!` と表示されていますが、２回目では表示されていません。つまり、プロパティにアクセスされるまでは `lazy` の引数であるラムダ式が実行されず、アクセスされるとラムダ式が実行されます。２回目以降はラムダ式は実行されずに、値がキャッシュされ利用されます。

ちなみに、ラムダ式の中で例外が発生すると値をキャッシュできないので２回目以降もラムダ式が実行されます。

``` kotlin
>>> val throwException: String by lazy {
...     throw Exception()
...     "Hello"
... }
>>> println(throwException)
java.lang.Exception
	at Line_9$throwException$2.invoke(Line_9.kts:2)
	at Line_9$throwException$2.invoke(Line_9.kts:1)
	at kotlin.SynchronizedLazyImpl.getValue(LazyJVM.kt:74)
	at Line_9.getThrowException(Line_9.kts)
>>> println(throwException)
java.lang.Exception
	at Line_9$throwException$2.invoke(Line_9.kts:2)
	at Line_9$throwException$2.invoke(Line_9.kts:1)
	at kotlin.SynchronizedLazyImpl.getValue(LazyJVM.kt:74)
	at Line_9.getThrowException(Line_9.kts)
>>>
```

# delegate property によるオブザーバー

オブザーバーも [`observable()`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.properties/-delegates/observable.html) 関数によって実現できます。以下に例を示します。

``` kotlin
>>> import kotlin.properties.Delegates
>>> class User {
...     var name: String by Delegates.observable("<no name>") {
...         prop, old, new ->
...         println("$old -> $new")
...     }
... }
>>> val user = User()
>>> user.name = "first"
<no name> -> first
>>> user.name = "second"
first -> second
```

値が変化するたびに `observable` の引数であるラムダ式が実行されているのがわかります。

もし、値のセットを拒否したい場合には [`vetoable()`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.properties/-delegates/vetoable.html) を使うことで実現できます。以下に例を示します。

``` kotlin
>>> import kotlin.properties.Delegates
>>> import kotlin.text.startsWith
>>> var name: String by Delegates.vetoable("no name") {
...     prop, old, new ->
...     !new.startsWith("N")
... }
>>> println(name)
no name
>>> name = "first"
>>> println(name)
first
>>> name = "NNN"
>>> println(name)
first
```

新しくセットされる値が `N` から始まっている場合、セットされないようにしてみました。出力を見てみると `NNN` がセットされていないことがわかります。

# おわりに

Delegate Property を使うことで色々便利な機能がシンプルに実装できる事がわかりました。このような機能が標準で提供されることは非常に嬉しいですね。問題を解決する手段が一つ増えたので、今後活用していきたいです。

# 参考文献

* [Kotlin | Delegate Property](https://kotlinlang.org/docs/reference/delegated-properties.html)