委托类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
interface DB {
fun save()
}

class SqlDB() : DB {
override fun save() { println("save to sql") }
}

class GreenDaoDB() : DB {
override fun save() { println("save to GreenDao") }
}
// 参数 通过 by 将接口实现委托给 db
// ↓ ↓
class UniversalDB(db: DB) : DB by db

fun main() {
UniversalDB(SqlDB()).save()
UniversalDB(GreenDaoDB()).save()
}

/*
输出:
save to sql
save to GreenDao
*/

等价于以下 Java 代码

1
2
3
4
5
6
7
class UniversalDB implements DB {
DB db;
public UniversalDB(DB db) { this.db = db; }
// 手动重写接口,将 save 委托给 db.save()
@Override// ↓
public void save() { db.save(); }
}

Kotlin 的委托类提供了语法层面的委托模式

通过这个 by 关键字,就可以自动将接口里的方法委托给一个对象,从而可以帮我们省略很多接口方法适配的模板代码。

委托属性

Kotlin“委托类”委托的是接口方法,而“委托属性”委托的,则是属性的 getter、setter。

标准委托

将属性 A 委托给属性 B

1
2
3
4
5
6
class Item {
var count: Int = 0
// ① ②
// ↓ ↓
var total: Int by ::count
}
1
2
3
4
5
6
7
8
9
10
11
12
// 近似逻辑,实际上,底层会生成一个Item$total$2类型的delegate来实现

class Item {
var count: Int = 0

var total: Int
get() = count

set(value: Int) {
count = value
}
}

这个特性,其实对我们软件版本之间的兼容很有帮助。假设 Item 是服务端接口的返回数据,1.0 版本的时候,我们的 Item 当中只 count 这一个变量:

1
2
3
4
// 1.0 版本
class Item {
var count: Int = 0
}

而到了 2.0 版本的时候,我们需要将 count 修改成 total,这时候问题就出现了,如果我们直接将 count 修改成 total,我们的老用户就无法正常使用了。但如果我们借助委托,就可以很方便地实现这种兼容。我们可以定义一个新的变量 total,然后将其委托给 count,这样的话,2.0 的用户访问 total,而 1.0 的用户访问原来的 count,由于它们是委托关系,也不必担心数值不一致的问题。

懒加载委托

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//            定义懒加载委托
// ↓ ↓
val data: String by lazy {
request()
}

fun request(): String {
println("执行网络请求")
return "网络数据"
}

fun main() {
println("开始")
println(data)
println(data)
}

结果:
开始
执行网络请求
网络数据
网络数据

懒加载委托的源代码,你会发现,它其实是一个高阶函数:

1
2
3
4
5
6
7
8
9
public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)


public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
when (mode) {
LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
}

自定义委托

自定义委托,我们必须遵循 Kotlin 制定的规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class StringDelegate(private var s: String = "Hello") {
// ① ② ③
// ↓ ↓ ↓
operator fun getValue(thisRef: Owner, property: KProperty<*>): String {
return s
}
// ① ② ③
// ↓ ↓ ↓
operator fun setValue(thisRef: Owner, property: KProperty<*>, value: String) {
s = value
}
}

// ②
// ↓
class Owner {
// ③
// ↓
var text: String by StringDelegate()
}
  • 首先,看到两处注释①对应的代码,对于 var 修饰的属性,我们必须要有 getValue、setValue 这两个方法,同时,这两个方法必须有 operator 关键字修饰。
  • 其次,看到三处注释②对应的代码,我们的 text 属性是处于 Owner 这个类当中的,因此 getValue、setValue 这两个方法中的 thisRef 的类型,必须要是 Owner,或者是 Owner 的父类。也就是说,我们将 thisRef 的类型改为 Any 也是可以的。一般来说,这三处的类型是一致的,当我们不确定委托属性会处于哪个类的时候,就可以将 thisRef 的类型定义为“Any?”。
  • 最后,看到三处注释③对应的代码,由于我们的 text 属性是 String 类型的,为了实现对它的委托,getValue 的返回值类型,以及 setValue 的参数类型,都必须是 String 类型或者是它的父类。大部分情况下,这三处的类型都应该是一致的。

这样的写法实在很繁琐,也可以借助 Kotlin 提供的 ReadWriteProperty、ReadOnlyProperty 这两个接口,来自定义委托。

1
2
3
4
5
6
7
8
9
public fun interface ReadOnlyProperty<in T, out V> {
public operator fun getValue(thisRef: T, property: KProperty<*>): V
}

public interface ReadWriteProperty<in T, V> : ReadOnlyProperty<T, V> {
public override operator fun getValue(thisRef: T, property: KProperty<*>): V

public operator fun setValue(thisRef: T, property: KProperty<*>, value: V)
}

如果我们需要为 val 属性定义委托,我们就去实现 ReadOnlyProperty 这个接口;如果我们需要为 var 属性定义委托,我们就去实现 ReadWriteProperty 这个接口。这样做的好处是,通过实现接口的方式,IntelliJ 可以帮我们自动生成 override 的 getValue、setValue 方法。

1
2
3
4
5
6
7
8
class StringDelegate(private var s: String = "Hello"): ReadWriteProperty<Owner, String> {
override operator fun getValue(thisRef: Owner, property: KProperty<*>): String {
return s
}
override operator fun setValue(thisRef: Owner, property: KProperty<*>, value: String) {
s = value
}
}

提供委托(provideDelegate)

假设我们现在有一个这样的需求:我们希望 StringDelegate(s: String) 传入的初始值 s,可以根据委托属性的名字的变化而变化。我们应该怎么做?

实际上,要想在属性委托之前再做一些额外的判断工作,我们可以使用 provideDelegate 来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class SmartDelegator {

operator fun provideDelegate(
thisRef: Owner,
prop: KProperty<*>
): ReadWriteProperty<Owner, String> {

return if (prop.name.contains("log")) {
StringDelegate("log")
} else {
StringDelegate("normal")
}
}
}

class Owner {
var normalText: String by SmartDelegator()
var logText: String by SmartDelegator()
}

fun main() {
val owner = Owner()
println(owner.normalText)
println(owner.logText)
}

结果:
normal
log

可以看到,为了在委托属性的同时进行一些额外的逻辑判断,我们使用创建了一个新的 SmartDelegator,通过它的成员方法 provideDelegate 嵌套了一层,在这个方法当中,我们进行了一些逻辑判断,然后再把属性委托给 StringDelegate。

如此一来,通过 provideDelegate 这样的方式,我们不仅可以嵌套 Delegator,还可以根据不同的逻辑派发不同的 Delegator。

实战与思考

案例 1:属性可见性封装

1
2
3
4
5
6
7
8
class Model {
val data: List<String> by ::_data
private val _data: MutableList<String> = mutableListOf()

fun load() {
_data.add("Hello")
}
}

在上面的代码中,我们定义了两个变量,一个变量是公开的“data”,它的类型是 List,这是 Kotlin 当中不可修改的 List,它是没有 add、remove 等方法的。

接着,我们通过委托语法,将 data 的 getter 委托给了 _data 这个属性。而 _data 这个属性的类型是 MutableList,这是 Kotlin 当中的可变集合,它是有 add、remove 方法的。由于它是 private 修饰的,类的外部无法直接访问,通过这种方式,我们就成功地将修改权保留在了类的内部,而类的外部访问是不可变的 List,因此类的外部只能访问数据。

案例 2:数据与 View 的绑定

1
2
3
4
5
6
operator fun TextView.provideDelegate(value: Any?, property: KProperty<*>) = object : ReadWriteProperty<Any?, String?> {
override fun getValue(thisRef: Any?, property: KProperty<*>): String? = text
override fun setValue(thisRef: Any?, property: KProperty<*>, value: String?) {
text = value
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
val textView = findViewById<textView>(R.id.textView)

// ①
var message: String? by textView

// ②
textView.text = "Hello"
println(message)

// ③
message = "World"
println(textView.text)


结果:
Hello
World

案例 3:ViewModel 委托

1
2
3
// MainActivity.kt

private val mainViewModel: MainViewModel by viewModels()

我们先来看看 viewModels() 是如何实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public inline fun <reified VM : ViewModel> ComponentActivity.viewModels(
noinline factoryProducer: (() -> Factory)? = null
): Lazy<VM> {
val factoryPromise = factoryProducer ?: {
defaultViewModelProviderFactory
}

return ViewModelLazy(VM::class, { viewModelStore }, factoryPromise)
}

public interface Lazy<out T> {

public val value: T

public fun isInitialized(): Boolean
}

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 来动态调整委托逻辑。

    关注我