设计大型面向对象系统的一个常见问题就是由继承的实现导致的脆弱性。 当我们需要扩展一个类并重写某些方法时,代码就变得依赖你继承的那个类的实现细节了。当系统不断的演进并且基类的实现被修改或者新方法被添加进去时,我们做出的关于类的行为的假设会失效,所以代码也许最后就以不正确的行为告终了。
Kotlin 的设计意识到了这种问题,并默认将实现类视作 final 的。这确保了只有那些设计成可扩展的类才可以被继承。当使用这样的类的时候,看到它是开放的,就会注意这些修改需要与派生类兼容。
另外,Kotlin 中引入了新的语法——委托,通过它我们可以代替多继承来解决问题。 委托是一种设计模式。它的基本理念是:操作对象不会自己去处理某段逻辑,而是会把工作委托给另外的一个辅助对象去处理。 比如调用 A 类的 methodA 方法,其实是背后的 B 类的 methodB 去执行。
委托对于 Java 程序员比较陌生,因为 Java 对委托没有语言层级的实现,而像 C# 等语言就对委托进行了原生的支持。
在 Kotlin 中只需要通过 by 关键字就可以实现委托的效果。 比如 by lazy 就是利用委托实现的延迟初始化语法。
Kotlin 中也是支持委托功能的,并且将委托功能分为两种:类委托和委托属性。
1 类委托,使用 by 关键字
首先来看类委托,它的核心思想在于将一个类的具体实现委托给另一个类去完成。
例如 Set 这种数据结构,Set 是一个接口,如果要使用它的话,需要使用它具体的实现类,比如 HashSet。借助委托模式,我们可以轻松实现一个自己的实现类。比如这里定义一个 CustomSet,并让它实现 Set 接口。
class CustomSet<T>(val helperSet: HashSet<T>) : Set<T> {
override val size: Int
get() = helperSet.size
override fun isEmpty(): Boolean = helperSet.isEmpty()
override fun iterator(): Iterator<T> = helperSet.iterator()
override fun containsAll(elements: Collection<T>): Boolean = helperSet.containsAll(elements)
override fun contains(element: T): Boolean = helperSet.contains(element)
}
在 CustomSet 的构造函数中接收了一个 HashSet 参数,这就相当于一个辅助对象。然后在 Set 接口所有的方法实现中,我们都没有进行自己的实现,而是调用了辅助对象中相应的方法实现,这就是一种委托模式。
那么,这种写法的好处是什么呢?既然都是调用辅助对象的方法实现,那还不如直接使用辅助对象的了。这么说确实没错,但如果我们只是让大部分的方法实现调用辅助对象中的方法,少部分的方法实现由自己来重写,甚至加入一些自己独有的方法,那么 CustomSet 就会成为一个全新的数据结构类,这就是委托模式的意义所在。
再比如,我们常常需要向其他类中添加一些行为,即使它并没有被设计为可扩展的。一个常用的实现方式是装饰器。 这种模式是指在不改变现有对象结构的情况下,动态的给对象增加一些额外的功能。 例如,下面就是需要多少代码来实现一个简单的如 Collection 的接口的装饰器,即使不需要修改任何的行为:
class DelegatingCollection<T> : Collection<T> {
private val innerList = arrayListOf<T>()
override val size: Int
get() = innerList.size
override fun isEmpty(): Boolean = innerList.isEmpty()
override fun iterator(): Iterator<T> = innerList.iterator()
override fun containsAll(elements: Collection<T>): Boolean = innerList.containsAll(elements)
override fun contains(element: T): Boolean = innerList.contains(element)
}
但是这种写法也有一定的弊端,如果接口中的待实现方法比较少还好,要是有几十甚至上百个方法的话,每个都去这样调用辅助对象中的相应方法实现,将会是很大的代码量。
那么这个问题有没有什么解决方法呢?在 Java 中没有,但是在 Kotlin 中可以通过类委托的功能来解决。
Kotlin 中委托使用的关键字是 by,我们只需要在接口声明的后面使用 by 关键字,再接上受委托的辅助对象,就可以免去之前所写的一大堆模版式的代码了。 如下所示:
class CustomSet<T>(val helperSet: HashSet<T>) : Set<T> by helperSet {
}
这两段代码的实现效果是一摸一样的,但是借住了类委托功能之后,代码明显简化了太多。另外,如果我们要对某个方法进行重新实现,只需要单独重写那一个方法就可以了,其他的方法仍然可以享受类委托带来的便利:
class CustomSet<T>(val helperSet: HashSet<T>) : Set<T> by helperSet {
fun helloWorld() = println("Hello World")
override fun isEmpty() = false
}
这里新增了一个 helloWorld() 方法,并且重写了 isEmpty() 方法,让它永远返回 false。现在 CustomSet 就成为了一个全新的数据结构类。它不仅永远不为空,而且还能打印 “Hello World”,至于其他的 Set 接口中的功能,则和 HashSet 保持一致。这就是 Kotlin 的类委托所能实现的功能。
再比如:
class DelegatingCollection<T>(
innerList: Collection<T> = ArrayList<T>()
) : Collection<T> by innerList {
}
类中所有的方法的实现都消失了。编译器会生成它们,并且实现与 DelegatingCollection 的例子是相似的。因为代码中没有太多意思的内容,所以当编译器能够自动为我们做同样的事情的时候就没有必要手写这些代码。
下面使用这种技术来实现一个集合。它可以计算向它添加元素的尝试次数。例如,在执行某种去重操作,可以使用这样的集合,通过比较添加元素的尝试次数和集合的最终大小来评判这种处理的效率。
class CountingSet<T>(
val innerSet: MutableCollection<T> = HashSet<T>()
) : MutableCollection<T> by innerSet { // 将 MutableCollection 的实现委托给 innerSet
var objectsAdded = 0
override fun add(element: T): Boolean {
objectsAdded++
return innerSet.add(element)
}
override fun addAll(elements: Collection<T>): Boolean {
objectsAdded += elements.size
return innerSet.addAll(elements)
}
}
val cset = CountingSet<Int>()
cset.addAll(listOf(1, 1, 2))
println("${cset.objectsAdded} objects were added, ${cset.size} remain")
// 3 objects were added, 2 remain
上面的代码,通过重写 add 和 addAll 方法来计数,并将 MutableCollection 接口剩下的实现委托给包装的容器。
重要的是没有对底层的集合的实现方式引入任何的依赖。 例如,不用关心集合是不是通过循环调用 add 来实现 addAll,或者是否是针对特定情况进行了优化的另一种实现。程序员对客户端代码调用类时会发生什么有完全的控制,并且只以来底层集合文档列出的 API 来实现操作,所以我们也可以依赖它来继续工作。
类委托可以代替多继承实现需求:
interface CanFly {
fun fly()
}
interface CanEat {
fun eat()
}
open class Flyer : CanFly {
override fun fly() {
println("I can fly")
}
}
open class Animal : CanEat {
override fun eat() {
println("I can eat")
}
}
class Bird(flyer: Flyer, animal: Animal) : CanFly by flyer, CanEat by animal { }
fun main() {
val flyer = Flyer()
val animal = Animal()
val b = Bird(flyer, animal)
b.fly()
b.eat()
}
委托方式和接口实现多继承很相似,而且好像也没有简单多少,和组合也很像,那么它的优势到底是什么呢?
- 接口是无状态的,所及即使它提供了默认方法实现也是很简单的,不能实现复杂的逻辑,也不推荐在接口中实现复杂的方法逻辑。这个时候,我们就可以用委托的方式,虽然也是接口委托,但是它是用一个具体的类去实现方法逻辑,可以拥有更强大的逻辑;
- 假设我们需要继承的类是 A,委托对象是 B、C。我们在具体调用的时候并不像组合那样 A.B.method,而是可以直接调用 A.method,这更能表达 A 拥有该 method 的能力,更加直观,虽然背后也是通过委托对象来执行具体的方法逻辑的;
2 属性委托
类委托的核心思想是将一个类的具体实现委托给另一个类去完成,而委托属性的核心思想是将一个属性(字段)的具体实现委托给另一个类去完成。
下面是委托属性的语法结构:
class CustomClass {
var p by Delegate()
}
可以看到,这里使用 by 关键字连接了左边的 p 属性和右边的 Delegate() 实例,这是什么意思呢?
属性 p 将它的访问器逻辑委托给了另一个对象:这里是 Delegate 类的一个新的实例。通过关键字 by 对其后面的表达式求值来获取这个对象,关键字 by 可以用于任何符合属性委托约定规则的对象。 当调用 p 属性的时候会自动调用 Delegate 类的 getValue() 方法,当给 p 属性赋值的时候会自动调用 Delegate 类的 setValue() 方法。
因此,我们还得对 Delegate 类进行具体的实现才行,代码如下所示:
class Delegate {
var propValue: Any? = null
operator fun getValue(customClass: CustomClass, prop: KProperty<*>): Any? {
return propValue
}
operator fun setValue(customClass: CustomClass, prop: KProperty<*>, value: Any?) {
propValue = value
}
}
这是一种标准的代码实现模版,在 Delegate 类中我们必须实现 getValue() 和 setValue() 这两个方法,并且都有使用 operator 关键字进行声明。
getValue() 方法要接收两个参数:第一个参数用于实现该 Delegate 类的委托功能,可以在什么类中使用,这里写成 CustomClass 表示仅可在 CustomClass 类中使用;第二个参数 KProperty<*>
是 Kotlin 中的一个属性操作类,可用于获取各种属性相关的值,在当前场景下用不着,但是必须在方法参数上进行声明。另外,<*> 这种泛型的写法表示你不知道或者不关心泛型的具体类型,只是为了通过语法编译而已,有点类似于 Java 中的 <?> 的写法。至于返回值可以声明成任何类型,根据具体的实现逻辑去写就行了,上述代码只是一种示例写法。
setValue() 方法也是相似的,只不过它要接收 3 个参数。前两个参数和 getValue() 方法是相同的,最后一个参数表示具体要赋值给委托属性的值,这个参数的类型必须和 getValue() 方法返回值的类型保持一致。
整个委托属性的工作流程就是这样实现的,现在当我们给 CustomClass 的 p 属性赋值时,就会调用 Delegate 类中实现 setValue() 方法,那就是 CustomClass 中的 p 属性是使用 val 关键字声明的。只一点很好理解,如果 p 属性是使用 val 关键字生命的,那么就意味着 p 属性是无法在初始化之后被重新赋值的,因此也就没有必要实现 setValue() 方法,只需要实现 getValue() 方法就可以了。
有些常见的属性操作,可以通过委托的方式实现。例如:
- lazy 延迟属性,只在第一次访问该属性时才根据需要创建对象的一部分;
- observable 可观察属性,属性发生改变时通知;
- map 集合,将属性存在一个 map 集合里面;
2.1 使用委托属性:惰性初始化和 by lazy()
by lazy {} 惰性初始化其实就是利用委托实现的延迟初始化,直到在第一次访问该属性的时候,才根据需要创建对象的一部分。 以下是 by lazy { } 的使用:
val laziness: String by lazy { // by lazy实现延迟初化效果
println("I will have a value")
"I am a lazy-initialized string"
}
这里使用了一种懒加载技术,把想要延迟执行的代码放到 by lazy { } 代码块中,这样代码块中的代码一开始不会执行,只有当 laziness 变量首次被调用的时候,代码块中的代码才会执行。
它的基本语法结构如下:
val p by lazy { ... }
by lazy 并不是连在一起的关键字,只有 by 才是 Kotlin 中的关键字,lazy 在这里只是一个高阶函数而已。
在 lazy 函数中会创建并返回一个 Delegate 对象,当我们调用 p 属性的时候,其实调用的是 Delegate 的 getValue() 方法,然后 getValue() 方法中有会调用 lazy 函数传入的 Lambda 表达式,这样表达式中的代码就可以执行了,并且调用 p 属性后得到的值就是 Lambda 表达式中最后一行代码的返回值。
在实际开发的过程中,当初始化过程消耗大量资源并且在使用对象时并不总是需要数据时,这个非常有用。
举例来说,一个 Person 类,可以用来访问一个人写的邮件列表。邮件存储在数据库中,访问比较耗时。我们希望只有在首次访问时才加载邮件,并只执行一次。假设已经有函数 loadEmails,用来从数据库中检索电子邮件:
class Person(val name: String) {
}
class Email {
}
fun loadEmails(person: Person): List<Email> {
println("Load emails for ${person.name}")
return listOf(...)
}
下面展示了如何使用额外的 _emails 属性来实现惰性加载,在没有加载之前为 null,然后加载邮件列表。
class Person(val name: String) {
private var _emails: List<Email>? = null
val emails: List<Email>
get() {
if (_emails == null) {
_emails = loadEmails(this)
}
return _emails!!
}
}
val p = Person("Eileen")
p.emails // Load emails for Eileen
p.emails
这里使用了所谓的支持属性技术。有一个属性,_emails
,用来存储这个值,而另一个 emails,用来提供对属性的读取访问。就是说需要使用两个属性,因为属性具有不同的类型:_emails
可以为空,而 emails 为非空。
但是这个代码有点繁琐:要是有几个惰性属性就会很多,而且,它并不总是正常运行,这个实现是线程不安全的。使用委托属性会让代码变得简单的多,可用于封装与存储值的支持属性和确保该值只被初始化一次的逻辑。在这理可以使用标准函数库 lazy 返回的委托。
class Person(val name: String) {
val emails by lazy { loadEmails(this) }
}
Lazy 函数返回一个对象,该对象具有一个名为 getValue 且签名正确的方法,因此可以把它和 by 关键字 一起使用来创建一个委托属性。lazy 的参数是一个 lambda,可以调用它来初始化这个值。默认情况下,lazy 函数是线程安全的,如果需要,可以设置其他选项来告诉它要使用那个锁,或者完全避开同步,如果该类永远不会在多线程环境中使用。
这样看来,Kotlin 的懒加载技术也没有那么神秘,掌握了它的原理后,就可以实现一的自定义的 lazy 函数。新建一个 Later.kt 文件,并编写如下代码:
class Later<T>(val block: () -> T) { }
这里我们首先定义了一个 Later 类,并将它指定成泛型类。Later 的构造函数中接收一个函数类型参数,这个函数类型参数不接收任何参数,并且返回值类型就是 Later 类指定的泛型。
接着我们在 Later 类中实现 getValue() 方法,代码如下所示:
class Later<T>(val block: () -> T) {
var value: Any? = null
operator fun getValue(any: Any?, prop: KProperty<*>): T {
if (value == null) {
value = block()
}
return value as T
}
}
这里将 getValue() 方法的第一个参数指定成了 Any? 类型,表示我们希望 Later 的委托功能在所有的类中都可以使用。然后使用了一个 value 变量对值进行缓存,如果 value 为空就调用构造函数中传入的函数类型去获取值,否则就直接返回。
由于懒加载技术是不会对属性进行赋值的,因此这里我们不不用实现 setValue() 方法了。
代码写到这里,委托属性的功能已经完成了。虽然我们可以立刻使用它,不过为了让它的用法更加类似于 lazy 函数,最好再定义一个顶层函数。这个函数直接写在 Later.kt 文件中就可以了,但是要定义在 Later 类的外面,因为只有不定义在任何类当中的函数才是顶层函数。代码如下所示:
fun <T> later(block: ()-> T) = Later(block)
我们将这个顶层函数也定义成泛型函数,并且它也接收一个函数类型参数。这个顶层函数的作用很简单:创建 Later 类的实例,并将接收的函数类型参数传给 Later 类的构造函数。
现在,我们自己编写的 later 懒加载就已经完成了,你可以直接使用它来替代之前的 lazy 函数,如下所示:
val uriMatcher by later {
val matcher = UriMatcher(UriMatcher.NO_MATCH)
matcher.addURI(authority, "book", bookDir)
matcher.addURI(authority, "book/#", bookItem)
matcher.addURI(authority, "category", categoryDir)
matcher.addURI(authority, "category/#", categoryItem)
}
但是如何才能验证 later 函数的懒加载功能有没有生效?这里我有一个非常简单方便的验证方法,写法如下:
val p by later {
println("run codes inside later block")
"test later"
}
可以看到,我们在 later 函数的代码块中打印了一行日志。将这段代码放到任何一个 Activity 中,并在按钮的点击时间里调用 p 属性。
你会发现,当 Activity 启动的时候,later 函数中的那行日志是不会打印的。只有当你首次点击按钮的时候,日志才会打印出来,说明代码块中的代码成功执行了。而当你再次点击按钮的时候,日志也不会打印出来,因为代码块中的代码只会执行一次。
通过这种方式就可以验证懒加载功能到底有没有生效了。
另外,必须说明的是,虽然我们编写了一个自己的懒加载函数,但由于简单起见,这里只是大致还原了 lazy 函数的基本实现原理,在一些诸如同步、空值处理等方面没有实现的很严谨。因此在正式的项目中,使用 Kotlin 内置的 lazy 函数才是最佳的选择。
2.2 实现委托属性
当一个对象的属性更改时通知监听器。这在许多不同的情况下都有用。例如,当对象显示在 UI 时,希望在对象变化时 UI 能自动刷新。Java 具有用于此类通知的标准机制:PropertyChangeSupport 和 PropertyChangeEvent 类。
在 Kotlin 中,如果不使用委托属性,该怎样实现?
PropertyChangeSupport 类中维护了一个监听器列表,并向它们发送 PropertyChangeEvent 事件。要使用它,通常需要把这个类的一个实例存储为 bean 类的一个字段,并将属性更改的处理委托给它。
为了避免要在每个类中去添加这个字段,需要创建一个小的工具类,用来存储 PropertyChangeSupport 的实例并监听属性更改。之后,定义一个继承这个工具类的类,以访问 changeSupport。
// PropertyChangeSupport 的工具类
open class PropertyChangeAware {
protected val changeSupport = PropertyChangeSupport(this)
fun addPropertyChangeListener(listener: PropertyChangeListener) {
changeSupport.addPropertyChangeListener(listener)
}
fun removePropertyChangeListener(listener: PropertyChangeListener) {
changeSupport.removePropertyChangeListener(listener)
}
}
接下来写一个 Person 类,定义一个只读属性(作为一个人的名称,一般不会随时更改)和两个可写属性:年龄和工资。当这个人的年龄或工资发生改变时,这个类将通知它的监听器。
// 手动实现属性修改的通知
class Person(val name: String, age: Int, salary: Int) : PropertyChangeAware() {
var age: Int = age
set(newValue) {
val oldValue = field // field 标识符允许我们访问属性背后的支持字段
field = newValue
// 当属性变化时,通知监听器
changeSupport.firePropertyChange("age", oldValue, newValue)
}
var salary: Int = salary
set(newVale) {
val oldValue = field
field = newVale
changeSupport.firePropertyChange("salary", oldValue, newVale)
}
}
fun main() {
val p = Person("Eileen", 34, 2000)
p.addPropertyChangeListener( // 关联监听器,用于监听属性修改
PropertyChangeListener { event ->
println(
"Property ${event.propertyName} change " +
"from ${event.oldValue} to ${event.newValue}"
)
}
)
p.age = 35
}
注意,这里的代码是如何使用 field 标识符来访问 age 和 salary 属性的支持字段。
setter 中有很多重复的代码。下面我们来尝试提取一个类,用来存储这个属性的值并发起通知。
class ObservableProperty(
val propName: String, var propValue: Int,
val changeSupport: PropertyChangeSupport
) {
fun getValue(): Int = propValue
fun setValue(newValue: Int) {
val oldValue = propValue
propValue = newValue
changeSupport.firePropertyChange(propName, oldValue, newValue)
}
}
class Person(val name: String, age: Int, salary: Int) : PropertyChangeAware() {
val _age = ObservableProperty("age", age, changeSupport)
var age: Int
get() = _age.getValue()
set(value) {
_age.setValue(value)
}
val _salary = ObservableProperty("salary", salary, changeSupport)
var salary: Int
get() = _salary.getValue()
set(value) {
_salary.setValue(value)
}
}
现在大致了解在 Kotlin 中,委托属性是如何工作的。
我们创建了一个保存属性值的类,并在修改属性时自动触发更改通知。删除了重复的逻辑代码,但是需要相当多的样板代码来为每个属性创建 ObservableProperty 实例,并把 getter 和 setter 委托给它。
Kotlin 的委托属性功能可以让我们摆脱这些样板代码。但是在此之前,需要更改 ObservableProperty 方法的签名,来匹配 Kotlin 约定所需的方法。
class ObservableProperty(
var propValue: Int, val changeSupport: PropertyChangeSupport
) {
operator fun getValue(p: Person, prop: KProperty<*>): Int = propValue
operator fun setValue(p: Person, prop: KProperty<*>, newValue: Int) {
val oldValue = propValue
propValue = newValue
changeSupport.firePropertyChange(prop.name, oldValue, newValue)
}
}
与之前的版本相比,这次代码做了一些更改:
- 现在,按照约定的需要,getValue 和 setValue 函数被标记了 operator;
- 这些函数加了俩参数:一个用于接收属性的实例,用来设置或读取属性,另一个用于表示属性本身。这个属性类型为 KProperty,可以通过 KProperty.name 的方式来访问该属性的名称;
- 把 name 属性从主构造方法中删除了,因为现在已经可以通过 KProperty 访问属性名称;
下面看 Person 的代码:
class Person(val name: String, age: Int, salary: Int) : PropertyChangeAware() {
var age = ObservableProperty(age, changeSupport)
var salary = ObservableProperty(salary, changeSupport)
}
通过关键字 by,Kotlin 编译器会自动执行之前版本的代码手动完成的操作。如果把这份代码与以前版本的 Person 类进行比较:使用委托属性时生成的代码非常类似。右边的对象称为委托。Kotlin 会自动将委托存储在隐藏的属性中,并在访问或修改属性时调用委托的 getValue 和 setValue。
我们不需要手动去实现可观察的属性逻辑,可以使用 Kotlin 标准库,它已经包含了类似于 ObservableProperty 的类。标准库类和这里使用的 PropertyChangeSupport 类没有耦合,因此只需要传递一个 lambda,来告诉它如何通知属性值的更改:
class Person(val name: String, age: Int, salary: Int) : PropertyChangeAware() {
private val observer = { prop: KProperty<*>, oldValue: Int, newValue: Int ->
changeSupport.firePropertyChange(prop.name, oldValue, newValue)
}
var age: Int by Delegates.observable(age, observer)
var salary: Int by Delegates.observable(salary, observer)
}
by 右边的表达式不一定是新创建的实例,也可以是函数调用、另一个属性或任何其他表达式,只要这个表达式的值,是能够被编译器用正确的参数类型来调用 getValue 和 setValue 的对象。与其他约定一样,getValue 和 setValue 可以是对象自己声明的方法或扩展函数。
2.3 委托属性的变换规则
下面总结一下委托属性是怎么工作的,假设我们已经有了一个具有委托属性的类:
class C {
var prop: Type by MyDelegate()
}
val c = C()
MyDelegate 实例会被保存到一个隐蔽的属性中,它被称为 <delegate>
。编译器也将用一个 KProperty 类型的对象来代表这个属性,它被称为 <property>
。
编译器生成的代码如下:
class C {
private val <delegate> = MyDelegate()
var prop: Type
get() = <delegate>.getValue(this, <property>)
set(value :Type) = <delegate>.setValue(this,<property>,value)
}
因此,在每个属性访问器中,编译器都会生成对应的 getValue 和 setValue 方法,如下所示:
当访问属性时,会调用 <delegate>
的 getValue 和 setValue 函数。
这个机制非常简单,但它可以实现许多有趣的场景。我们可以自定义存储该属性值的位置(map、数据库表或者用户会话的 Cookie 中),以及在访问该属性时做点什么(比如添加验证、更改通知等)。所有这一切都可以用紧凑的代码完成。
3 在 map 中保存属性值
委托属性发挥作用的另一种常见方法,是用在有动态定义的属性集的对象中。这样的对象有时被称为自订(expando)对象。
例如,考虑一个联系人管理系统,可以用来存储有关联系人的任意信息。系统中的每个人都有一些属性需要特殊处理(例如名字),以及每个人特有的数量任意的额外属性(例如,最小的孩子的生日)。实现这种系统的一种方法就是将人的所有属性存储在 map 中,不确定提供属性,来访问需要特殊处理的信息。
下面是例子:
class Person {
private val _attributes = hashMapOf<String, String>()
fun setAttribute(attrName: String, value: String) {
_attributes[attrName] = value
}
val name: String
get() = _attributes["name"]!!
}
fun main() {
val p = Person()
val data = mapOf("name" to "Dmitry", "company" to "JetBrains")
for ((attrName, value) in data) {
p.setAttribute(attrName, value)
}
println(p.name) // Dmitry
}
这里使用了一个通用的 API 来把数据加载到对象中(在实际项目中,可以是 JSON 反序列化或类似的方法),然后使用特定的 API 来访问一个属性的值。把它改为委托属性非常简单,可以直接将 map 放在 by 关键字后面。
class Person {
private val _attributes = hashMapOf<String, String>()
fun setAttribute(attrName: String, value: String) {
_attributes[attrName] = value
}
val name: String by _attributes // 把 map 作为委托属性
}
因为标准库已经在标准 Map 和 MultableMap 接口上定义了 getValue 和 setValue 扩展函数,所以这里可以直接这样用。属性的名称将自动用作在 map 中的键,属性值作为 map 中的值。