mtkw.dev

Kotlinのdelegateについて勉強する

これは何?

Kotlinには2つのDelegateがあります。

  • プロパティデリゲート
  • クラスデリゲート

ComposeのStateに対するbyや、by lazyなどもおまじないではなく言語仕様から理解したくお勉強していきます。

プロパティデリゲートとは?

val name: String
    get() = ...
    set(value) = ...

プロパティにはsetter/getterがありますが、これを別のオブジェクトに委譲して再利用したい場合にプロパティデリゲートが利用できます。

fun main() {
    val user = User()
    val car = Car()
 
    user.name = "Mike" // this is the setter of User.
    car.name = "Mike's car" // this is the setter of Car.
}
 
class User {
    var name: String? by NameDelegate()
}
 
class Car {
    var name: String? by NameDelegate()
}
 
class NameDelegate {
    var name: String? = null
 
    operator fun getValue(
        thisRef: Any,
        property: KProperty<*>
    ): String? {
        println("this is the getter of ${thisRef::class.simpleName}.")
        return name
    }
 
    operator fun setValue(
        thisRef: Any,
        property: KProperty<*>,
        value: String?,
    ) {
        println("this is the setter of ${thisRef::class.simpleName}.")
        this.name = value
    }
}

これは例です。nameに関するsetter/getterで共通してログを出力したいとします。その場合、各User/Carクラスの実装するのではなく、Delegateクラスを利用することで共通化できます。

byはoperator関数に処理を委譲するキーワードです。

  • valプロパティに対するby
    • getValue operator関数が必要
  • varプロパティに対するby
    • getValue operator関数が必要
    • setValue operator関数が必要

プロパティデリゲートは拡張関数でも実現可能

プロパティデリゲートのためにDelegateクラスを用意するのはちょっと面倒ですよね。byが必要としているのはoperator関数なのでclassは必須ではありません。そのため拡張関数としても定義可能です。前述のNameDelegateは以下のように実装可能です。

fun main() {
    val user = User()
    val car = Car()
 
    user.name = "Mike" // this is the setter of User.
    car.name = "Mike's car" // this is the setter of Car.
}
 
class User {
    // var name: NameState = NameState()
    var name: String? by NameState() // NameStateからの値の取り出しをbyでoperator関数に委譲する
}
 
class Car {
    // var name: NameState = NameState()
    var name: String? by NameState() // NameStateからの値の取り出しをbyでoperator関数に委譲する
}
 
class NameState {
    var value: String? = null
}
 
operator fun NameState.getValue(
    thisRef: Any,
    property: KProperty<*>
): String? {
    println("this is the getter of ${thisRef::class.simpleName}.")
    return value
}
 
operator fun NameState.setValue(
    thisRef: Any,
    property: KProperty<*>,
    value: String?,
) {
    println("this is the setter of ${thisRef::class.simpleName}.")
    this.value = value
}

少しクラス名は変えましたが名前の状態を管理するStateクラスと、そのプロパティgetter/setterをoperator関数として定義しました。

プロパティデリゲート(例:ComposeのState)

これはComposeのStateに対するbyと同じです。

var name by remember { mutableStateOf("Taro") }

このように実装しますが、ここでのbyは何をしているのでしょうか?

まず remember { mutableStateOf("Taro") } で作成される値の型はMutableState<String>です。ここでのbyはMutableStateへのsetter/getterを委譲しています。

@Suppress("NOTHING_TO_INLINE")
public inline operator fun <T> State<T>.getValue(thisObj: Any?, property: KProperty<*>): T = value
 
@Suppress("NOTHING_TO_INLINE")
public inline operator fun <T> MutableState<T>.setValue(
    thisObj: Any?,
    property: KProperty<*>,
    value: T,
) {
    this.value = value
}

上記のoperator関数に処理を委譲してます。そのため name はString型として扱うことができています。

プロパティデリゲート(例:by lazy)

Kotlinでby lazyを行いますが、このbyもプロパティデリゲートです。

public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)

上記はlazy関数です。lazyはLazy<T>のインスタンを生成します。

public interface Lazy<out T> {
    /**
     * Gets the lazily initialized value of the current `Lazy` instance.
     * Once the value was initialized it must not change during the rest of lifetime of this `Lazy` instance.
     *
     * @sample samples.lazy.LazySamples.lazyExplicitSample
     */
    public val value: T
 
    /**
     * Returns `true` if a value for this `Lazy` instance has been already initialized, and `false` otherwise.
     * Once this function has returned `true` it stays `true` for the rest of lifetime of this `Lazy` instance.
     */
    public fun isInitialized(): Boolean
}

Lazyは value という値を持っています。

つまり、ここでのbyはLazy<T>への値のgetterの委譲です。

@kotlin.internal.InlineOnly
public inline operator fun <T> Lazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value

こちらがLazyへのgetValueの定義です。Lazyから値を取り出しています。

クラスデリゲートとは?

クラスデリゲートはinterfacの実装を別オブジェクトに丸投げする構文です。

例えば、Loggerが良い例です。

interface Logger {
    fun debug(message: String)
    fun info(message: String)
    fun error(message: String)
}
 
class ConsoleLogger : Logger {
    override fun debug(message: String) = println("DEBUG: $message")
    override fun info(message: String) = println("INFO: $message")
    override fun error(message: String) = println("ERROR: $message")
}

ここで新しいMyLoggerクラスを作成する場合を考えます。

class MyLogger(
    private val logger: Logger
) : Logger {
 
    override fun debug(message: String) = logger.debug(message)
 
    override fun info(message: String) = logger.info(message)
 
    override fun error(message: String) = logger.error(message)
}

このコードはただの横流しで冗長です。

class MyLogger(
    private val logger: Logger
) : Logger by logger

クラスデリゲートを利用すると上記になります。ここのbyはプロパティデリゲートのgetter/setterの委譲とは別のものです。