前言
- 委托(Delegate)是 Kotlin 的一种语言特性,用于更加优雅地实现委托模式;
- 在这篇文章里,我将总结 Kotlin 委托机制的使用方法 & 原理,如果能帮上忙,请务必点赞加关注,这真的对我非常重要。
- 本文相关代码可以从 DemoHall·KotlinDelegate 下载查看。
目录
1. 概述
- 什么是委托: 一个对象将消息委托给另一个对象来处理。
- Kotlin 委托解决了什么问题: Kotlin 通过 by 关键字可以更加优雅地实现委托。
2. Kotlin 委托基础
- 类委托: 一个类的方法不在该类中定义,而是直接委托给另一个对象来处理。
- 属性委托: 一个类的属性不在该类中定义,而是直接委托给另一个对象来处理。
- 局部变量委托: 一个局部变量不在该方法中定义,而是直接委托给另一个对象来处理。
2.1 类委托
Kotlin 类委托的语法格式如下:
class <类名>(b : <基础接口>) : <基础接口> by <基础对象> 复制代码
举例:
// 基础接口 interface Base { fun print() } // 基础对象 class BaseImpl(val x: Int) : Base { override fun print() { print(x) } } // 被委托类 class Derived(b: Base) : Base by b fun main(args: Array<String>) { val b = BaseImpl(10) Derived(b).print() // 最终调用了 Base#print() } 复制代码
基础类和被委托类都实现同一个接口,编译时生成的字节码中,继承自 Base 接口的方法都会委托给基础对象处理。
2.2 属性委托
Kotlin 属性委托的语法格式如下:
val/var <属性名> : <类型> by <基础对象> 复制代码
举例:
class Example { // 被委托属性 var prop: String by Delegate() // 基础对象 } // 基础类 class Delegate { private var _realValue: String = "彭" operator fun getValue(thisRef: Any?, property: KProperty<*>): String { println("getValue") return _realValue } operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { println("setValue") _realValue = value } } fun main(args: Array<String>) { val e = Example() println(e.prop) // 最终调用 Delegate#getValue() e.prop = "Peng" // 最终调用 Delegate#setValue() println(e.prop) // 最终调用 Delegate#getValue() } 输出: getValue 彭 setValue getValue Peng 复制代码
基础类不需要实现任何接口,但必须提供 getValue() 方法,如果是委托可变属性,还需要提供 setValue()。在每个属性委托的实现的背后,Kotlin 编译器都会生成辅助属性并委托给它。 例如,对于属性 prop,会生成「辅助属性」 prop$delegate。 而 prop 的 getter() 和 setter() 方法只是简单地委托给辅助属性的 getValue() 和 setValue() 处理。
源码: class Example { // 被委托属性 var prop: String by Delegate() // 基础对象 } -------------------------------------------------------- 编译器生成的字节码: class Example { private val prop$delegate = Delegate() // 被委托属性 var prop: String get() = prop$delegate.getValue(this, this:prop) set(value : String) = prop$delegate.setValue(this, this:prop, value) } 复制代码
注意事项:
- thisRef —— 必须与属性所有者类型相同或者是它的超类型。
- property —— 必须是类型 KProperty<*> 或其超类型。
- value —— 必须和属性同类型或者是它的超类型。
2.3 局部变量委托
局部变量也可以声明委托,例如:
fun main(args: Array<String>) { val lazyValue: String by lazy { println("Lazy Init Completed!") "Hello World." } if (true/*someCondition*/) { println(lazyValue) // 首次调用 println(lazyValue) // 后续调用 } } 输出: Lazy Init Completed! Hello World. Hello World. 复制代码
3. Kotlin 委托进阶
3.1 延迟属性委托 lazy
lazy 是一个标准库函数,参数为一个 Lambda 表达式,返回值为一个 Lazy 实例,使用 lazy 可以实现延迟属性委托,在委托对象比较耗资源的场景会非常有用。首次访问属性是,会执行 lazy 函数的 lambda 表达式并将结果记录到「背域」,后续调用 getter() 方法只是直接返回「背域」的值。 例如:
val lazyValue: String by lazy { println("Lazy Init Completed!") "Hello World." } fun main(args: Array<String>) { println(lazyValue) // 首次调用 println(lazyValue) // 后续调用 } 输出: Lazy Init Completed! Hello World. Hello World. 复制代码
3.2 可观察属性 ObservableProperty
使用 Delegates.observable() 可以实现可观察属性,函数接受两个参数:第一个参数为初始值,第二个参数为属性值变化的回调。函数的返回值是 ObservableProperty 可观察属性,它在调用 setValue(...) 是触发回调。例如:
class User { var name: String by Delegates.observable("初始值") { prop, old, new -> println("旧值:$old -> 新值:$new") } } fun main(args: Array<String>) { val user = User() user.name = "第一次赋值" user.name = "第二次赋值" } 输出: 旧值:初始值 -> 新值:第一次赋值 旧值:第一次赋值 -> 新值:第二次赋值 复制代码
3.3 使用 Map 存储属性值
Map / MutableMap 也可以用来实现属性委托,从而此时字段名是 Key,属性值是 Value。例如;
class User(val map: Map<String, Any?>) { val name: String by map } fun main(args: Array<String>) { val map = mutableMapOf( "name" to "彭" ) val user = User(map) println(user.name) map["name"] = "peng" println(user.name) } 输出: 彭 peng 复制代码
不过,这里有一个坑:如果 Map 中不存在委托属性名的映射值,在取值的时候会抛异常:Key $key is missing in the map.
。源码体现如下:
标准库·MapAccessors.kt
@kotlin.jvm.JvmName("getVar") @kotlin.internal.InlineOnly public inline operator fun <V, V1 : V> MutableMap<in String, out @Exact V>.getValue(thisRef: Any?, property: KProperty<*>): V1 = (getOrImplicitDefault(property.name) as V1) @kotlin.internal.InlineOnly public inline operator fun <V> MutableMap<in String, in V>.setValue(thisRef: Any?, property: KProperty<*>, value: V) { this.put(property.name, value) } 复制代码
标准库·MapWithDefault.kt
@kotlin.jvm.JvmName("getOrImplicitDefaultNullable") @PublishedApi internal fun <K, V> Map<K, V>.getOrImplicitDefault(key: K): V { if (this is MapWithDefault) return this.getOrImplicitDefault(key) return getOrElseNullable(key, { throw NoSuchElementException("Key $key is missing in the map.") }) } 复制代码
反正我是猜不透 Kotlin 官方为什么要加这个限制,所以项目里我不会直接使用标准库里的实现,而是采用以下自定义实现:
MapAccessors.kt
class MapAccessors(val map: MutableMap<String, Any?>) { public inline operator fun <V> getValue(thisRef: Any?, property: KProperty<*>): V = @Suppress("UNCHECKED_CAST") (map[property.name] as V) public inline operator fun <V> setValue(thisRef: Any?, property: KProperty<*>, value: V) { map[property.name] = value } } // 使用方法(其实用扩展函数语法更简洁,但考虑到编辑器不会帮我们导致自定义实现,所以故意套在 MapAccessors 内): private val _data = MapAccessors(HashMap<String, Any?>()) private var count: Int? by _data 复制代码
3.4 ReadOnlyProperty / ReadWriteProperty
实现属性委托或局部委托时,除了定义类 Delegate 外,还可以直接使用 Kotlin 标准库中的两个接口:ReadOnlyProperty / ReadWriteProperty。对于 val 变量使用 ReadOnlyProperty,而 var 变量实现ReadWriteProperty,使用这两个接口可以方便地让 IDE 帮你生成函数签名。例如:
val name by object : ReadOnlyProperty<Any?, String> { override fun getValue(thisRef: Any?, property: KProperty<*>): String { return "Peng" } } var name by object : ReadWriteProperty<Any?, String> { override fun getValue(thisRef: Any?, property: KProperty<*>): String { return "Peng" } override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { } } 复制代码
4. 在 Android 中使用 Kotlin 委托
4.1 Kotlin 委托 + Fragment / Activity 传参
我们经常需要在 Activity / Fragment 之间传递参数,类似以下代码:
OrderDetailFragment.kt
class OrderDetailFragment : Fragment(R.layout.fragment_order_detail) { private var orderId: Int? = null private var orderType: Int? = null companion object { const val EXTRA_ORDER_ID = "orderId" const val EXTRA_ORDER_TYPE = "orderType"; fun newInstance(orderId: Int, orderType: Int?) = OrderDetailFragment().apply { Bundle().apply { putInt(EXTRA_ORDER_ID, orderId) if (null != orderType) { putInt(EXTRA_ORDER_TYPE, orderType) } }.also { arguments = it } } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) arguments?.let { orderId = it.getInt(EXTRA_ORDER_ID, 10000) orderType = it.getInt(EXTRA_ORDER_TYPE, 2) } } } 复制代码
可以看到我们要为每个参数编写类似的模板代码,还要考虑参数为空的问题,而且 Int 基础类型不能使用 lateinit 关键字,你还不得不声明属性为可空类型,即使你可以确保它不会为空。
有没有办法收敛模板代码呢?这里就符合委托机制的应用场景了,我们可以把参数赋值和获取的代码抽取委托类,然后将 oderId 和 orderType 声明为「委托属性」。例如:
OrderDetailFragment.kt
class OrderDetailFragment : Fragment(R.layout.fragment_order_detail) { private lateinit var tvDisplay: TextView private var orderId: Int by argument() private var orderType: Int by argument(2) companion object { fun newInstance(orderId: Int, orderType: Int) = OrderDetailFragment().apply { this.orderId = orderId this.orderType = orderType } } override fun onViewCreated(root: View, savedInstanceState: Bundle?) { // Try to modify (UnExcepted) this.orderType = 3 // Display Value tvDisplay = root.findViewById(R.id.tv_display) tvDisplay.text = "orderId = $orderId, orderType = $orderType" } } 复制代码
干净清爽!相对于常规的写法,使用属性委托优势很明显:
- 1、样板代码减少: 不再需要定义 Key 字符串,而是直接使用变量名作为 Key;不再需要编写向 Argument 设置参数和读取参数的代码;
- 2、非空参数可以声明 val: 可空参数和非空参数区分两种委托,现在非空参数也可以声明为 val 了;
- 3、清晰地设置可空参数默认值: 声明可空参数时可以顺便声明默认值。
除了 Fragment 传参,Activity 传参也可以使用委托属性。完整代码和演示工程你可以直接下载查看:下载路径,这里只展示部分核心代码如下:
ArgumentDelegate.kt
fun <T> fragmentArgument() = FragmentArgumentProperty<T>() class FragmentArgumentProperty<T> : ReadWriteProperty<Fragment, T> { override fun getValue(thisRef: Fragment, property: KProperty<*>): T { return thisRef.arguments?.getValue(property.name) as? T ?: throw IllegalStateException("Property ${property.name} could not be read") } override fun setValue(thisRef: Fragment, property: KProperty<*>, value: T) { val arguments = thisRef.arguments ?: Bundle().also { thisRef.arguments = it } if (arguments.containsKey(property.name)) { // The Value is not expected to be modified return } arguments[property.name] = value } } 复制代码
4.2 Kotlin 委托 + ViewBinding
ViewBinding 是 Android Gradle Plugin 3.6 中新增的特性,用于更加轻量地实现视图绑定,可以理解为轻量版本的 DataBinding。ViewBinding 的使用方法和实现原理都很好理解,但常规的使用方法存在一些局限性:
- 1、创建和回收 ViewBinding 对象需要重复编写样板代码,特别是在 Fragment 中使用的案例;
- 2、binding 属性是可空的,也是可变的,使用起来不方便。
使用 Kotlin 属性委托可以非常优雅地解决这两个问题,优化前后对比:
TestFragment.kt
class TestFragment : Fragment(R.layout.fragment_test) { private var _binding: FragmentTestBinding? = null private val binding get() = _binding!! override fun onViewCreated(root: View, savedInstanceState: Bundle?) { _binding = FragmentTestBinding.bind(root) binding.tvDisplay.text = "Hello World." } override fun onDestroyView() { super.onDestroyView() _binding = null } } 复制代码
优化后:
TestFragment.kt
class TestFragment : Fragment(R.layout.fragment_test) { private val binding by viewBinding(FragmentTestBinding::bind) override fun onViewCreated(root: View, savedInstanceState: Bundle?) { binding.tvDisplay.text = "Hello World." } } 复制代码
干净清爽!详细分析过程你直接看我的另一篇文章:Android | ViewBinding 与 Kotlin 委托双剑合璧
5. 总结
Kotlin 委托的语法关键字是 by,其本质上是面向编译器的语法糖,三种委托(类委托、对象委托和局部变量委托)在编译时都会转化为 “无糖语法”。例如类委托:编译器会实现基础接口的所有方法,并直接委托给基础对象来处理。例如对象委托和局部变量委托:在编译时会生成辅助属性(prop$degelate),而属性 / 变量的 getter() 和 setter() 方法只是简单地委托给辅助属性的 getValue() 和 setValue() 处理。