[毎日Kotlin] Day36. Delegates(委譲)

2018.03.14

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

毎日Kotlinシリーズです。

このシリーズを初めての方はこちらです。「毎日Kotlin」はじめました | Developers.IO

問題

Delegates how it works | Try Kotlin

You may declare your own delegates. Implement the methods of the class 'EffectiveDate' so it can be delegated to. Store only the time in milliseconds in 'timeInMillis' property.

Use the extension functions MyDate.toMillis() and Long.toDate(), defined at MyDate.kt

import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

class D {
    var date: MyDate by EffectiveDate()
}

class EffectiveDate<R> : ReadWriteProperty<R, MyDate> {

    var timeInMillis: Long? = null

    override fun getValue(thisRef: R, property: KProperty<*>): MyDate {
        TODO()
    }

    override fun setValue(thisRef: R, property: KProperty<*>, value: MyDate) {
        TODO()
    }
}
import java.util.Calendar

data class MyDate(val year: Int, val month: Int, val dayOfMonth: Int)

fun MyDate.toMillis(): Long {
    val c = Calendar.getInstance()
    c.set(year, month, dayOfMonth, 0, 0, 0)
    c.set(Calendar.MILLISECOND, 0)
    return c.getTimeInMillis()
}

fun Long.toDate(): MyDate {
    val c = Calendar.getInstance()
    c.setTimeInMillis(this)
    return MyDate(c.get(Calendar.YEAR), c.get(Calendar.MONTH), c.get(Calendar.DATE))
}

狙い

ここで考えて欲しい問題の意図はなんだろうか?

ライブラリーなどを作るときに重宝します。Javaではなかった機能なので何度か練習は必要です。

解答例

class EffectiveDate<R> : ReadWriteProperty<R, MyDate> {

    var timeInMillis: Long? = null

    override fun getValue(thisRef: R, property: KProperty<*>): MyDate {
        return timeInMillis!!.toDate()
    }

    override fun setValue(thisRef: R, property: KProperty<*>, value: MyDate) {
        timeInMillis = value.toMillis()
    }
}

簡単に言うと、getterとsetterのかわりを行うクラスを作り、それに委譲するイメージです。

たとえば、スタンダードライブラリーではDelegatesnotNullの定義をみてみましょう。

Delegates.kt at 1.2.30 · JetBrains/kotlin

private class NotNullVar<T: Any>() : ReadWriteProperty<Any?, T> {
    private var value: T? = null

    public override fun getValue(thisRef: Any?, property: KProperty<*>): T {
        return value ?: throw IllegalStateException("Property ${property.name} should be initialized before get.")
    }

    public override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        this.value = value
    }
}

ReadWriteProperty<Any?, T>の部分ですが、Any?型で定義できるプロパティを限定しています。この場合は、Any?なのですべてのクラスでこの委譲プロパティがつかえます。たとえば、Hogeクラだけで使用する場合は、ReadWriteProperty<Hoge, T>にします。

2つ目のTは、setterで入力される型と同時にgetterで返される型を指定します。当然といえば当然ですが、setterでいれる型もgetterで取得できる型も同じですね。notNullの場合は、どのクラスでも使用できるようにTにして、宣言時に決めます。常にStringだけ返すような場合は、ReadWriteProperty<Any?, String>にします。

すごいシンプルな例を、みんな大好きHello , worldでやってみます。

class Hello {
    var string by WorldDelegate()

}

class WorldDelegate : ReadWriteProperty<Hello, String> {

    var v: String = ""

    override fun getValue(thisRef: Hello, property: KProperty<*>): String {
        return v
    }

    override fun setValue(thisRef: Hello, property: KProperty<*>, value: String) {
        v = value + " , world"
    }

}

class DelegateTest {
    @Test
    fun test() {
        val hoge = Hello()
        hoge.string = "hello"
        Assert.assertEquals("hello , world", hoge.string)
    }
}

ReadWritePropertyはvarですが、valのときはReadOnlyPropertyを使えば、getterだけの定義をすればよいです。

operatorを使った定義

operatorを使って、同様に定義することができます。

class WorldDelegate : ReadWriteProperty<Hello, String> {

    var v: String = ""

    override fun getValue(thisRef: Hello, property: KProperty<*>): String {
        return v
    }

    override fun setValue(thisRef: Hello, property: KProperty<*>, value: String) {
        v = value + " , world"
    }

}
class WorldDelegate {

    var v: String = ""

    operator fun getValue(thisRef: Hello, property: KProperty<*>): String {
        return v
    }

    operator fun setValue(thisRef: Hello, property: KProperty<*>, value: String) {
        v = value + " , world"
    }

}

Javaに戻してみる

インスタンスをつくって、getter,setterをそのインスタンスのgetter,setterに流してるだけなんですけどね。

public final class Hello {
   // $FF: synthetic field
   static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.mutableProperty1(new MutablePropertyReference1Impl(Reflection.getOrCreateKotlinClass(Hello.class), "string", "getString()Ljava/lang/String;"))};
   @NotNull
   private final WorldDelegate string$delegate = new WorldDelegate();

   @NotNull
   public final String getString() {
      return this.string$delegate.getValue(this, $$delegatedProperties[0]);
   }

   public final void setString(@NotNull String var1) {
      Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
      this.string$delegate.setValue(this, $$delegatedProperties[0], var1);
   }
}

あとがき

Day37.でまたお会いしましょう。