Kotlin笔记 09 - 委托
委托类
1 | interface DB { |
等价于以下 Java 代码
1 | class UniversalDB implements DB { |
Kotlin 的委托类提供了语法层面的委托模式
通过这个 by 关键字,就可以自动将接口里的方法委托给一个对象,从而可以帮我们省略很多接口方法适配的模板代码。
委托属性
Kotlin“委托类”委托的是接口方法,而“委托属性”委托的,则是属性的 getter、setter。
标准委托
将属性 A 委托给属性 B
1 | class Item { |
1 | // 近似逻辑,实际上,底层会生成一个Item$total$2类型的delegate来实现 |
这个特性,其实对我们软件版本之间的兼容很有帮助。假设 Item 是服务端接口的返回数据,1.0 版本的时候,我们的 Item 当中只 count 这一个变量:
1 | // 1.0 版本 |
而到了 2.0 版本的时候,我们需要将 count 修改成 total,这时候问题就出现了,如果我们直接将 count 修改成 total,我们的老用户就无法正常使用了。但如果我们借助委托,就可以很方便地实现这种兼容。我们可以定义一个新的变量 total,然后将其委托给 count,这样的话,2.0 的用户访问 total,而 1.0 的用户访问原来的 count,由于它们是委托关系,也不必担心数值不一致的问题。
懒加载委托
1 | // 定义懒加载委托 |
懒加载委托的源代码,你会发现,它其实是一个高阶函数:
1 | public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer) |
自定义委托
自定义委托,我们必须遵循 Kotlin 制定的规则
1 | class StringDelegate(private var s: String = "Hello") { |
- 首先,看到两处注释①对应的代码,对于 var 修饰的属性,我们必须要有 getValue、setValue 这两个方法,同时,这两个方法必须有 operator 关键字修饰。
- 其次,看到三处注释②对应的代码,我们的 text 属性是处于 Owner 这个类当中的,因此 getValue、setValue 这两个方法中的 thisRef 的类型,必须要是 Owner,或者是 Owner 的父类。也就是说,我们将 thisRef 的类型改为 Any 也是可以的。一般来说,这三处的类型是一致的,当我们不确定委托属性会处于哪个类的时候,就可以将 thisRef 的类型定义为“Any?”。
- 最后,看到三处注释③对应的代码,由于我们的 text 属性是 String 类型的,为了实现对它的委托,getValue 的返回值类型,以及 setValue 的参数类型,都必须是 String 类型或者是它的父类。大部分情况下,这三处的类型都应该是一致的。
这样的写法实在很繁琐,也可以借助 Kotlin 提供的 ReadWriteProperty、ReadOnlyProperty 这两个接口,来自定义委托。
1 | public fun interface ReadOnlyProperty<in T, out V> { |
如果我们需要为 val 属性定义委托,我们就去实现 ReadOnlyProperty 这个接口;如果我们需要为 var 属性定义委托,我们就去实现 ReadWriteProperty 这个接口。这样做的好处是,通过实现接口的方式,IntelliJ 可以帮我们自动生成 override 的 getValue、setValue 方法。
1 | class StringDelegate(private var s: String = "Hello"): ReadWriteProperty<Owner, String> { |
提供委托(provideDelegate)
假设我们现在有一个这样的需求:我们希望 StringDelegate(s: String) 传入的初始值 s,可以根据委托属性的名字的变化而变化。我们应该怎么做?
实际上,要想在属性委托之前再做一些额外的判断工作,我们可以使用 provideDelegate 来实现。
1 | class SmartDelegator { |
可以看到,为了在委托属性的同时进行一些额外的逻辑判断,我们使用创建了一个新的 SmartDelegator,通过它的成员方法 provideDelegate 嵌套了一层,在这个方法当中,我们进行了一些逻辑判断,然后再把属性委托给 StringDelegate。
如此一来,通过 provideDelegate 这样的方式,我们不仅可以嵌套 Delegator,还可以根据不同的逻辑派发不同的 Delegator。
实战与思考
案例 1:属性可见性封装
1 | class Model { |
在上面的代码中,我们定义了两个变量,一个变量是公开的“data”,它的类型是 List,这是 Kotlin 当中不可修改的 List,它是没有 add、remove 等方法的。
接着,我们通过委托语法,将 data 的 getter 委托给了 _data 这个属性。而 _data 这个属性的类型是 MutableList,这是 Kotlin 当中的可变集合,它是有 add、remove 方法的。由于它是 private 修饰的,类的外部无法直接访问,通过这种方式,我们就成功地将修改权保留在了类的内部,而类的外部访问是不可变的 List,因此类的外部只能访问数据。
案例 2:数据与 View 的绑定
1 | operator fun TextView.provideDelegate(value: Any?, property: KProperty<*>) = object : ReadWriteProperty<Any?, String?> { |
1 | val textView = findViewById<textView>(R.id.textView) |
案例 3:ViewModel 委托
1 | // MainActivity.kt |
我们先来看看 viewModels() 是如何实现的:
1 | public inline fun <reified VM : ViewModel> ComponentActivity.viewModels( |
viewModels() 是 Activity 的一个扩展函数。也是因为这个原因,我们才可以直接在 Activity 当中直接调用 viewModels() 这个方法。
另外,我们注意到,viewModels() 这个方法的返回值类型是 Lazy,那么,它是如何实现委托功能的呢?
1 | public inline operator fun <T> Lazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value |
Lazy 类在外部还定义了一个扩展函数 getValue(),这样,我们的只读属性的委托就实现了。而 Android 官方这样的代码设计,就再一次体现了职责划分、关注点分离的原则。Lazy 类只包含核心的成员,其他附属功能,以扩展的形式在 Lazy 外部提供。
小结
- 委托类,委托的是接口的方法,它在语法层面支持了“委托模式”。
- 委托属性,委托的是属性的 getter、setter。虽然它的核心理念很简单,但我们借助这个特性可以设计出非常复杂的代码。
- 另外,Kotlin 官方还提供了几种标准的属性委托,它们分别是:两个属性之间的直接委托、by lazy 懒加载委托、Delegates.observable 观察者委托,以及 by map 映射委托;
- 两个属性之间的直接委托,它是 Kotlin 1.4 提供的新特性,它在属性版本更新、可变性封装上,有着很大的用处;
- by lazy 懒加载委托,可以让我们灵活地使用懒加载,它一共有三种线程同步模式,默认情况下,它就是线程安全的;Android 当中的 viewModels() 这个扩展函数在它的内部实现的懒加载委托,从而实现了功能强大的 ViewModel;
- 除了标准委托以外,Kotlin 可以让我们开发者自定义委托。自定义委托,我们需要遵循 Kotlin 提供的一套语法规范,只要符合这套语法规范,就没问题;
- 在自定义委托的时候,如果我们有灵活的需求时,可以使用 provideDelegate 来动态调整委托逻辑。
关注我