Kotlin | 委托机制 & 原理 & 应用 彭旭锐

简介: Kotlin | 委托机制 & 原理 & 应用彭旭锐

前言


  • 委托(Delegate)是 Kotlin 的一种语言特性,用于更加优雅地实现委托模式;
  • 在这篇文章里,我将总结 Kotlin 委托机制的使用方法 & 原理,如果能帮上忙,请务必点赞加关注,这真的对我非常重要。
  • 本文相关代码可以从 DemoHall·KotlinDelegate 下载查看。


目录

image.png


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() 处理。

目录
相关文章
|
4天前
|
移动开发 API Android开发
构建高效Android应用:Kotlin协程的实践指南
【5月更文挑战第11天】 在移动开发领域,性能优化和资源管理是至关重要的。特别地,对于Android开发者来说,合理利用Kotlin协程可以极大地改善应用的响应性和稳定性。本文将深入探讨Kotlin协程在Android中的实际应用,包括它们如何简化异步编程模型、提高UI线程的响应性,以及减少内存消耗。我们将通过具体案例分析,了解如何在实际项目中有效地使用协程,从而帮助开发者构建更加高效的Android应用程序。
|
2天前
|
移动开发 Android开发 UED
构建高效Android应用:探究Kotlin协程的优势与实践
【5月更文挑战第17天】 在移动开发领域,尤其是针对Android平台,性能优化和流畅的用户体验始终是开发者追求的目标。Kotlin作为一种现代的编程语言,自引入Android开发以来,其简洁、安全和互操作性的特点受到广泛欢迎。特别是Kotlin协程的推出,为解决Android平台上的并发编程问题提供了新的思路。本文将深入探讨Kotlin协程的核心优势,并通过实例展示如何在Android应用中有效利用协程来提高响应性和稳定性,从而改善整体的用户体验。
|
2天前
|
移动开发 API Android开发
构建高效Android应用:探究Kotlin协程的优势与实践
【5月更文挑战第17天】在移动开发领域,性能优化和流畅的用户体验一直是开发者追求的目标。针对Android平台,Kotlin语言凭借其简洁性和功能丰富性成为了许多开发者的首选。其中,Kotlin协程作为异步编程的强大工具,为处理并发任务提供了轻量级的解决方案。本文深入探讨了Kotlin协程的核心优势,并通过实例分析其在Android开发中的应用,旨在帮助开发者提升应用的性能和响应能力。
|
2天前
|
数据库 Android开发 开发者
打造高效Android应用:Kotlin协程的全面应用
【5月更文挑战第17天】随着移动开发技术的不断进步,开发者寻求更高效的编程模式来提升应用性能和用户体验。本文深入探讨了Kotlin协程在Android开发中的应用,揭示了如何利用这一现代并发解决方案来简化异步编程,提升应用响应速度,并确保用户界面的流畅性。通过实例分析,我们将展示Kotlin协程如何与Android框架无缝集成,以及它们在处理网络请求、数据库操作和耗时任务时的优势。
8 1
|
2天前
|
移动开发 Android开发 开发者
构建高效Android应用:探究Kotlin协程的优势与实践
【5月更文挑战第17天】 在移动开发领域,尤其是针对Android平台,性能优化和流畅的用户体验始终是开发者追求的目标。近年来,Kotlin语言因其简洁性和功能性而成为Android开发的热门选择。其中,Kotlin协程作为一种轻量级的线程管理方案,为编写异步代码提供了强大支持,使得处理并发任务更加高效和容易。本文将深入探讨Kotlin协程的核心优势,并通过具体实例展示如何在Android应用中有效利用协程来提升性能和用户体验。
|
2天前
|
移动开发 Android开发 UED
构建高效Android应用:探究Kotlin协程的优势与实践
【5月更文挑战第17天】 在移动开发领域,性能优化和流畅的用户体验始终是核心追求。针对Android平台,Kotlin协程作为一种新兴的轻量级线程管理方案,正逐渐改变开发者对于异步编程和后台任务处理的认识。本文通过深入分析Kotlin协程的原理、优势以及在实际Android应用中的使用案例,揭示了如何利用协程提高应用性能,减少资源消耗,并最终实现更流畅的用户体验。我们将通过一系列实验证据,展示协程如何在不牺牲可读性和可维护性的前提下,提升代码执行效率,并为Android开发社区提供一种新的并发处理范式。
|
2天前
|
移动开发 调度 Android开发
构建高效Android应用:Kotlin协程的全面应用
【5月更文挑战第17天】随着移动开发技术的不断进步,开发者寻求更高效、响应更快的应用程序。在Android平台上,Kotlin作为一种现代编程语言,提供了协程这一强大的并发处理工具。本文深入探讨了如何在Android应用中使用Kotlin协程来提升性能和用户体验,同时保证代码的简洁性和可维护性。我们将分析协程的核心概念,并通过实例展示其在实际开发中的应用。
|
2天前
|
移动开发 安全 Android开发
构建高效Android应用:Kotlin与协程的完美结合
【5月更文挑战第17天】 在移动开发领域,性能优化和流畅的用户体验是关键。对于Android平台而言,Kotlin语言凭借其简洁性和功能安全性成为开发的首选。与此同时,协程作为一种新的并发处理方式,在简化异步编程方面展现出巨大潜力。本文将深入探讨如何通过Kotlin语言以及协程技术,提升Android应用的性能和响应能力,并确保用户界面的流畅性。
|
3天前
|
安全 Java Android开发
构建高效Android应用:探究Kotlin与Java的性能差异
【5月更文挑战第16天】 在移动开发领域,性能一直是开发者关注的焦点。随着Kotlin语言的普及,其与Java在Android应用中的性能表现成为热门话题。本文将深入分析Kotlin和Java在Android平台上的性能差异,并通过实际测试数据来揭示二者在编译速度、应用启动时间以及运行效率方面的表现。我们的目标是为开发者提供一个参考依据,以便在选择合适的编程语言时做出更加明智的决策。
|
4天前
|
移动开发 数据处理 Android开发
构建高效Android应用:Kotlin协程的实践与优化策略
【5月更文挑战第14天】在移动开发领域,性能优化和资源管理是提升用户体验的关键因素之一。随着Kotlin语言的普及,其异步编程解决方案——协程,已经成为Android开发者手中的强大工具。本文将深入探讨Kotlin协程在Android应用中的实践方法,分析其在处理异步任务时带来的优势,并提出一系列优化策略,帮助开发者构建更加高效、响应迅速的Android应用。通过具体案例分析和性能对比,我们将展示如何充分利用协程来简化代码结构,提高应用性能,并确保用户界面的流畅性。