Menu
Coddy logo textTech

byを使った委譲

CoddyのKotlinジャーニー「オブジェクト指向プログラミング」セクションの一部。レッスン 46/57。

委譲とは、オブジェクトが継承するのではなく、別のオブジェクトに処理を任せることです。Kotlin はこれを直接サポートしています。class Polite(private val inner: Greeter) : Greeter by inner はすべての呼び出しを inner に転送することで Greeter を実装し、変更する部分だけをクラスでオーバーライドします:

interface Greeter {
    fun hello(name: String): String
    fun bye(name: String): String
}
class English : Greeter {
    override fun hello(name: String) = "Hello, $name"
    override fun bye(name: String) = "Bye, $name"
}
class Polite(private val inner: Greeter) : Greeter by inner {
    override fun hello(name: String) = inner.hello(name) + ", pleased to meet you"
}

main の中では:

val g = Polite(English())
println(g.hello("Ada"))
println(g.bye("Ada"))

出力:

Hello, Ada, pleased to meet you
Bye, Ada

委譲は継承の代替手段です。実装が final であるインターフェースに対して機能し、委譲先はプログラムの実行中に選択された任意のオブジェクトにできます。この集合はすべての add をカウントし、それ以外のすべてを通常の MutableSet に転送します:

class CountingSet<T>(private val inner: MutableSet<T> = mutableSetOf()) : MutableSet<T> by inner {
    var attempts = 0
        private set

    override fun add(element: T): Boolean {
        attempts++
        return inner.add(element)
    }
}

main の内部:

val tags = CountingSet<String>()
tags.add("kotlin")
tags.add("jvm")
tags.add("kotlin")
println("${tags.size} tags after ${tags.attempts} adds, contains jvm: ${"jvm" in tags}")

出力:

2 tags after 3 adds, contains jvm: true

委譲プロパティは、byを使って、プロパティのゲッターとセッターを別のオブジェクトに委譲します。第2章で登場したlazyもそのような委譲先の1つであり、マップも別の委譲先になります。それぞれのプロパティは、自身の名前を持つエントリを読み取ります。

class User(values: Map<String, Any?>) {
    val name: String by values
    val age: Int by values
}

mainの中で次のようにします。

val u = User(mapOf("name" to "Ada", "age" to 36))
println("${u.name} is ${u.age}")

出力:

Ada is 36

独自のプロパティデリゲートを記述できます。これは operator fun getValue を持つクラスで、var には setValue も追加します。Kotlin はプロパティを読み書きするたびにこれらを呼び出します。この例では、代入されたテキストから空白を削除し、変更回数を数えます。

import kotlin.reflect.KProperty

class Trimmed {
    private var value = ""
    var changes = 0

    operator fun getValue(thisRef: Any?, property: KProperty<*>) = value
    operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: String) {
        value = newValue.trim()
        changes++
    }
}

class Form {
    val nameField = Trimmed()
    var name: String by nameField
}

main の中で:

val form = Form()
form.name = "  Ada  "
form.name = "Ada L. "
println("[${form.name}] after ${form.nameField.changes} changes")

出力:

[Ada L.] after 2 changes
challenge icon

チャレンジ

簡単

インターフェース Storage(put、get、keysを含む)と、その実装である MapStorage が提供されています。クラス委譲を使った2つのデコレーターを、それぞれ put のみをオーバーライドして作成してください。LoggingStorage(inner) は読み取り専用の log リストに put a=1 を記録して呼び出しを転送し、LimitedStorage(inner, max) は max 個のキーが存在する場合に新しいキーの追加を拒否(falseを返す)します。ただし、既存のキーの更新は引き続き許可されます。それ以外はすべて by を使って転送します。

提供されているコードは MapStorage を LoggingStorage でラップし、1行目が正の制限値の場合はさらに LimitedStorage でラップします。put a 1、get a、keys のコマンドを実行し、ok、refused、値または missing、そしてキーのリストを出力し、最後に log: put a=1, ... を出力します。

コードは LoggingStorage.kt、LimitedStorage.kt、Storage.kt、MapStorage.kt に記述してください。Main.kt には提供された入出力コードが含まれており、編集できません。

自分で試してみよう

fun main() {
    // 提供された入出力コード: そのままにしておく
    val input = generateSequence(::readLine).toList()
    val base = MapStorage()
    val logging = LoggingStorage(base)
    val limit = input[0].toInt()
    val storage: Storage = if (limit > 0) LimitedStorage(logging, limit) else logging
    for (cmd in input.drop(1)) {
        val p = cmd.split(" ")
        when (p[0]) {
            "put" -> println(if (storage.put(p[1], p[2])) "ok" else "refused")
            "get" -> println(storage.get(p[1]) ?: "missing")
            else -> println(storage.keys())
        }
    }
    println("log: " + logging.log.joinToString(", ").ifEmpty { "empty" })
}
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン

自分で練習してみよう: Kotlinプレイグラウンド