Menu
CoddyTech

使用 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

委托是继承的另一种选择。它适用于实现类不可继承的接口,并且委托对象可以是在程序运行时选择的任意对象。这个集合会统计每次 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 将属性的 getter 和 setter 交给另一个对象。第 2 章中的 lazy 就是这样一种委托,映射也可以作为委托:每个属性都会读取与自己同名的条目:

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 已提供。请使用类委托编写两个装饰器,每个装饰器只重写 put:LoggingStorage(inner) 会在其只读 log 列表中记录 put a=1,并转发调用;LimitedStorage(inner, max) 会在已有 max 个键时拒绝添加新键(返回 false),但仍允许更新已有键。其他所有内容都通过 by 转发。

提供的代码会将 MapStorage 包装在 LoggingStorage 中;如果第一行是正数限制值,还会再将其包装在 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 在线编辑器