はじめに
KotlinはよくJavaと比較されて話されることがありますが、Kotlinにあり、Javaにはない言語機能が多数あります。その中の一つに delegate property というものがあり調べてみました。
delegate property とは
delegate property とは名前の通り、「委譲されたプロパティ」です。もう少しわかり易い言葉で説明すると、変数のゲッター・セッターの処理を本来とは別のクラスに任せる事ができるわけです。処理が委譲できるとボイラーテンプレートや遅延評価などが可能になります。
使い方はとても簡単です。まず、委譲するクラスを実装し by
キーワードで変数を宣言するだけです。以下に例を示します。
class Example {
var p: String by Delegate()
}
by
キーワードで Delegate
クラスをインスタンス化し指定しました。Delegate
クラスは以下のようになっています。
// 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 で以上を実行してみました。結果は以下のようになります。
>>> 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()
関数を利用することで遅延評価を実現できます。REPLでやってみた結果を以下に示します。
>>> val lazyValue: String by lazy {
... println("computed!")
... "Hello"
... }
>>> println(lazyValue)
computed!
Hello
>>> println(lazyValue)
Hello
>>>
最初の呼び出しでは computed!
と表示されていますが、2回目では表示されていません。つまり、プロパティにアクセスされるまでは lazy
の引数であるラムダ式が実行されず、アクセスされるとラムダ式が実行されます。2回目以降はラムダ式は実行されずに、値がキャッシュされ利用されます。
ちなみに、ラムダ式の中で例外が発生すると値をキャッシュできないので2回目以降もラムダ式が実行されます。
>>> 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()
関数によって実現できます。以下に例を示します。
>>> 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()
を使うことで実現できます。以下に例を示します。
>>> 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 を使うことで色々便利な機能がシンプルに実装できる事がわかりました。このような機能が標準で提供されることは非常に嬉しいですね。問題を解決する手段が一つ増えたので、今後活用していきたいです。