search
LoginSignup
2

More than 1 year has passed since last update.

posted at

Organization

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

はじめに

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 を利用することで遅延評価などを実装できるのですが、よく使われるであろう機能については標準ライブラリですでに実装されています。その中でも代表的な lazyobservable を以下に示します。

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

参考文献

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
2