知识图库
Java知识库
JDK线程池实现原理
Java中的强、软、弱、虚引用
深入拆解Java虚拟机
01 开篇词 | 为什么我们要学习Java虚拟机?
02 Java代码是怎么运行的?
03 Java的基本类型
04 Java虚拟机是如何加载Java类的?
05 JVM是如何执行方法调用的?(上)
06 JVM是如何执行方法调用的?(下)
7 JVM是如何处理异常的?
Java面试常见问题整理
Java面试常见问题-Java 基础篇
Java面试常见问题-Jvm篇
Java面试常见问题-并发篇
Android知识库
Kotlin编程第一课
1 开篇词 | 入门Kotlin有多容易,精通Kotlin就有多难
2 Kotlin基础语法:正式开启学习之旅
3 面向对象:理解Kotlin设计者的良苦用心
4 Kotlin原理:编译器在幕后干了哪些“好事”?
5 实战:构建一个Kotlin版本的四则运算计算器
6 object关键字:你到底有多少种用法?
7 扩展:你的能力边界到底在哪里?
8 高阶函数:为什么说函数是Kotlin的“一等公民”?
9 实战:用Kotlin写一个英语词频统计程序
10 加餐一 | 初识Kotlin函数式编程
11 委托:你为何总是被低估?
12 泛型:逆变or协变,傻傻分不清?
13 注解与反射:进阶必备技能
14 实战:用Kotlin实现一个网络请求框架KtHttp
15 加餐二 | 什么是“表达式思维”?
16 加餐三 | 什么是“不变性思维”?
17 加餐四 | 什么是“空安全思维”?
18 春节刷题计划(一)| 当Kotlin遇上LeetCode
19 春节刷题计划(二)| 一题三解,搞定版本号判断
20 春节刷题计划(三)| 一题双解,搞定求解方程
21 春节刷题计划(四)| 一题三解,搞定分式加减法
22 什么是“协程思维模型”?
23 如何启动协程?
24 挂起函数:Kotlin协程的核心
25 Job:协程也有生命周期吗?
26 Context:万物皆为Context?
27 实战:让KtHttp支持挂起函数
28 期中考试 | 用Kotlin实现图片处理程序
29 题目解答 | 期中考试版本参考实现
30 Channel:为什么说Channel是“热”的?
31 Flow:为什么说Flow是“冷”的?
32 select:到底是在选择什么?
33 并发:协程不需要处理同步吗?
34 异常:try-catch居然会不起作用?坑!
35 实战:让KtHttp支持Flow
36 答疑(一)| Java和Kotlin到底谁好谁坏?
37 集合操作符:你也会“看完就忘”吗?
38 协程源码的地图:如何读源码才不会迷失?
39 图解挂起函数:原来你就是个状态机?
40 加餐五 | 深入理解协程基础元素
41 launch的背后到底发生了什么?
42 Dispatchers是如何工作的?
43 CoroutineScope是如何管理协程的?
44 图解Channel:如何理解它的CSP通信模型?
45 图解Flow:原来你是只纸老虎?
46 Java Android开发者还会有未来吗?
47 Kotlin与Jetpack简直是天生一对!
48 用Kotlin写一个GitHub Trending App
49 结课测试 | “Kotlin编程第一课”100分试卷等你来挑战!
50 结束语 | 不忘初心
Android Framework 教程—基础篇
01 Ubuntu 使用快速入门
02 Make 构建工具入门
03 理解 Unicode UTF-8 UTF-16 UTF-32
04 Linux Shell 脚本编程入门1——核心基础语法
05 SeAndroid 使用极速上手
06 理解 C++ 的 Memory Order
07 AOSP 极速上手
08 系统开发工具推荐
09 添加 Product
运动相关知识
爱上跑步
01 开篇词 | 跑步,不那么简单的事儿
02 跑两步就喘了,是不是我不适合跑步?
03 正确的跑步姿势是什么样的?
04 为什么跑步要先热身?
05 怎样制定你的第一个10公里跑步计划?
06 快跑和慢跑,哪个更燃脂?
07 普通跑步者应该如何选择跑鞋?
08 买跑步装备,不要踩这些坑儿
09 跑步前到底应不应该吃东西?
10 跑步到底伤不伤膝盖?
11 参加了20场马拉松,我是如何准备的?
12 除了马拉松,还能参加哪些跑步赛事?
13 热点问题答疑 :跑完第二天浑身疼,还要不要继续跑?
健身房计划
[DeepSeek]减脂塑形计划
【DeepSeek】训练周期安排
每日餐饮热量控制
减脂期间食物推荐避坑指南
HarmonyOS知识库
其他知识类目
心理学相关
如何学点心理学——关于非专业人士学心理学的一点建议
投射性认同
-
+
首页
11 委托:你为何总是被低估?
今天我们来学习Kotlin的委托特性。 Kotlin的委托主要有两个应用场景,一个是委托类,另一个是委托属性。对比第6讲我们学过的扩展来看的话,Kotlin委托这个特性就没有那么“神奇”了。 因为扩展可以从类的外部,为一个类“添加”成员方法和属性,因此Kotlin扩展的应用场景也十分明确,而Kotlin委托的应用场景就没那么清晰了。这也是很多人会“重视扩展”,而“轻视委托”的原因。 然而,我要告诉你的是,Kotlin“委托”的重要性一点也不比“扩展”低。Kotlin委托在软件架构中可以发挥巨大的作用,在掌握了Kotlin委托特性后,你不仅可以改善应用的架构,还可以大大提升开发效率。 另外,如果你是Android工程师,你会发现Jetpack Compose当中大量使用了Kotlin委托特性。可以说,如果你不理解委托,你就无法真正理解Jetpack Compose。 看到这里,想必你也已经知道Kotlin委托的重要性了,接下来就来开启我们的学习之旅吧! ### 委托类 我们先从委托类开始,它的使用场景非常简单易懂:它常常用于实现类的“委托模式”。我们来看个简单例子: ```kotlin 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 */ ``` 以上的代码当中,我们定义了一个DB接口,它的save()方法用于数据库存储,SqlDB和GreenDaoDB都实现了这个接口。接着,我们的UniversalDB也实现了这个接口,同时通过by这个关键字,将接口的实现委托给了它的参数db。 这种委托模式在我们的实际编程中十分常见,UniversalDB相当于一个壳,它虽然实现了DB这个接口,但并不关心它怎么实现。具体是用SQL还是GreenDao,传不同的委托对象进去,它就会有不同的行为。 另外,以上委托类的写法,等价于以下Java代码,我们可以再进一步来看下: ```java class UniversalDB implements DB { DB db; public UniversalDB(DB db) { this.db = db; } // 手动重写接口,将 save 委托给 db.save() @Override// ↓ public void save() { db.save(); } } ``` 以上代码显示,save()将执行流程委托给了传入的db对象。所以说,Kotlin的委托类提供了语法层面的委托模式。通过这个by关键字,就可以自动将接口里的方法委托给一个对象,从而可以帮我们省略很多接口方法适配的模板代码。 委托类很好理解,下面让我们重点来看看Kotlin的委托属性。 ### 委托属性 正如我们前面所讲的,Kotlin“委托类”委托的是接口方法,而“委托属性”委托的,则是属性的getter、setter。在第1讲中,我们知道val定义的属性,它只有get()方法;而var定义的属性,既有get()方法,也有set()方法。 那么,属性的getter、setter委托出去以后,能有什么用呢?我们可以从Kotlin官方提供的标准委托那里找到答案。 #### 委标准委托 Kotlin提供了好几种标准委托,其中包括两个属性之间的直接委托、by lazy懒加载委托、Delegates.observable观察者委托,以及by map映射委托。前面两个的使用频率比较高,后面两个频率比较低。这里,我们就主要来了解下前两种委托属性。 ##### 将属性A委托给属性B 从Kotlin 1.4 开始,我们可以直接在语法层面将“属性A”委托给“属性B”,就像下面这样: ```kotlin class Item { var count: Int = 0 // ① ② // ↓ ↓ var total: Int by ::count } ``` 以上代码定义了两个变量,count和total,其中total的值与count完全一致,因为我们把total这个属性的getter和setter都委托给了count。 注意,代码中的两处注释是关键: - 注释①,代表total属性的getter、setter会被委托出去; - 注释②,::count,代表total被委托给了count。这里的“::count”是属性的引用,它跟我们前面学过的函数引用是一样的概念。 total和count两者之间的委托关系一旦建立,就代表了它们两者的getter和setter会完全绑定在一起,如果要用代码来解释它们背后的逻辑,它们之间的关系会是这样: ```kotlin // 近似逻辑,实际上,底层会生成一个Item$total$2类型的delegate来实现 class Item { var count: Int = 0 var total: Int get() = count set(value: Int) { count = value } } ``` 也就是,当total的get()方法被调用时,它会直接返回count的值,也就意味着会调用count的get()方法;而当total的set()方法被调用时,它会将value传递给count,也就意味着会调用count的set()方法。 也许你会好奇:Kotlin 1.4提供的这个特性有啥用?为什么要分别定义count和total?我们直接用count不好吗? 这个特性,其实对我们软件版本之间的兼容很有帮助。假设Item是服务端接口的返回数据,1.0版本的时候,我们的Item当中只count这一个变量: ```kotlin // 1.0 版本 class Item { var count: Int = 0 } ``` 而到了2.0版本的时候,我们需要将count修改成total,这时候问题就出现了,如果我们直接将count修改成total,我们的老用户就无法正常使用了。但如果我们借助委托,就可以很方便地实现这种兼容。我们可以定义一个新的变量total,然后将其委托给count,这样的话,2.0的用户访问total,而1.0的用户访问原来的count,由于它们是委托关系,也不必担心数值不一致的问题。 好了,除了属性之间的直接委托以外,还有一种委托是我们经常会用到的,那就是懒加载委托。 ##### 懒加载委托 懒加载,顾名思义,就是对于一些需要消耗计算机资源的操作,我们希望它在被访问的时候才去触发,从而避免不必要的资源开销。前面第5讲学习单例的时候,我们就用到了by lazy的懒加载。其实,这也是软件设计里十分常见的模式,我们来看一个例子: ```kotlin // 定义懒加载委托 // ↓ ↓ val data: String by lazy { request() } fun request(): String { println("执行网络请求") return "网络数据" } fun main() { println("开始") println(data) println(data) } 结果: 开始 执行网络请求 网络数据 网络数据 ``` 通过“by lazy{}”,我们就可以实现属性的懒加载了。这样,通过上面的执行结果我们会发现:main()函数的第一行代码,由于没有用到data,所以request()函数也不会被调用。到了第二行代码,我们要用到data的时候,request()才会被触发执行。到了第三行代码,由于前面我们已经知道了data的值,因此也不必重复计算,直接返回结果即可。 并且,如果你去看懒加载委托的源代码,你会发现,它其实是一个高阶函数: ```kotlin public actual fun lazy(initializer: () -> T): Lazy = SynchronizedLazyImpl(initializer) public actual fun lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy = when (mode) { LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer) LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer) LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer) } ``` 可以看到,lazy()函数可以接收一个LazyThreadSafetyMode类型的参数,如果我们不传这个参数,它就会直接使用SynchronizedLazyImpl的方式。而且通过它的名字我们也能猜出来,它是为了多线程同步的。而剩下的SafePublicationLazyImpl、UnsafeLazyImpl,则不是多线程安全的。 好了,除了这两种标准委托以外,Kotlin也还提供了Delegates.observable观察者委托、by map映射委托,这两种委托比较简单,你可以点击这里给出的链接去了解它们的定义与用法。 #### 自定义委托 在学完Kotlin的标准委托以后,你也许会好奇:是否可以根据需求实现自己的属性委托呢?答案当然是可以的。 不过,为了自定义委托,我们必须遵循Kotlin制定的规则。 ```kotlin 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类型或者是它的父类。大部分情况下,这三处的类型都应该是一致的。 不过上面这段代码看起来还挺吓人的,刚开始的时候你也许会不太适应。但没关系,你只需要把它当作一个固定格式就行了。你在自定义委托的时候,只需要关心3个注释标注出来的地方即可。 而如果你觉得这样的写法实在很繁琐,也可以借助Kotlin提供的ReadWriteProperty、ReadOnlyProperty这两个接口,来自定义委托。 ```kotlin public fun interface ReadOnlyProperty { public operator fun getValue(thisRef: T, property: KProperty<*>): V } public interface ReadWriteProperty : ReadOnlyProperty { 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方法。 以前面的代码为例,我们的StringDelegate,也可以通过实现ReadWriteProperty接口来编写: ```kotlin class StringDelegate(private var s: String = "Hello"): ReadWriteProperty { 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来实现。 看看下面的SmartDelegator你就会明白: ```kotlin class SmartDelegator { operator fun provideDelegate( thisRef: Owner, prop: KProperty<*> ): ReadWriteProperty { 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。 ### 实战与思考 至此,我们就算是完成了Kotlin委托的学习,包括委托类、委托属性,还有4种标准委托模式。除了这些之外,我们还学习了如何自定义委托属性,其中包括我们自己实现getValue、setValue两个方法,还有通过实现ReadOnlyProperty、ReadWriteProperty这两个接口。而对于更复杂的委托逻辑,我们还需要采用provideDelegate的方式,来嵌套Delegator。 这里,为了让你对Kotlin委托的应用场景有一个更清晰的认识,我再带你一起来看看几个Android的代码案例。 #### 案例1:属性可见性封装 在软件设计当中,我们会遇到这样的需求:对于某个成员变量data,我们希望类的外部可以访问它的值,但不允许类的外部修改它的值。因此我们经常会写出类似这样的代码: ```kootlin class Model { var data: String = "" // ① private set private fun load() { // 网络请求 data = "请求结果" } } ``` 请留意代码注释①处,我们将data属性的set方法声明为private的,这时候,data属性的set方法只能从类的内部访问,这就意味着类的外部无法修改data的值了,但类的外部仍然可以访问data的值。 这样的代码模式很常见,我们在Java/C当中也经常使用,不过当我们的data类型从String变成集合以后,问题就不一样了。 ```kotlin class Model { val data: MutableList = mutableListOf() private fun load() { // 网络请求 data.add("Hello") } } fun main() { val model = Model() // 类的外部仍然可以修改data model.data.add("World") } ``` 对于集合而言,即使我们将其定义为只读变量val,类的外部一旦获取到data的实例,它仍然可以调用集合的add()方法修改它的值。这个问题在Java当中几乎没有优雅的解法。只要你暴露了集合的实例给外部,外部就可以随意修改集合的值。这往往也是Bug的来源,这样的Bug还非常难排查。 而在这个场景下,我们前面学习的“两个属性之间的委托”这个语法,就可以派上用场了。 ```kotlin class Model { val data: List by ::_data private val _data: MutableList = mutableListOf() fun load() { _data.add("Hello") } } ``` 在上面的代码中,我们定义了两个变量,一个变量是公开的“data”,它的类型是List,这是Kotlin当中不可修改的List,它是没有add、remove等方法的。 接着,我们通过委托语法,将data的getter委托给了_data这个属性。而_data这个属性的类型是MutableList,这是Kotlin当中的可变集合,它是有add、remove方法的。由于它是private修饰的,类的外部无法直接访问,通过这种方式,我们就成功地将修改权保留在了类的内部,而类的外部访问是不可变的List,因此类的外部只能访问数据。 #### 案例2:数据与View的绑定 在Android当中,如果我们要对“数据”与“View”进行绑定,我们可以用DataBinding,不过DataBinding太重了,也会影响编译速度。其实,除了DataBinding以外,我们还可以借助Kotlin的自定义委托属性来实现类似的功能。这种方式不一定完美,但也是一个有趣的思路。 这里我们以TextView为例: ```kotlin operator fun TextView.provideDelegate(value: Any?, property: KProperty<*>) = object : ReadWriteProperty { override fun getValue(thisRef: Any?, property: KProperty<*>): String? = text override fun setValue(thisRef: Any?, property: KProperty<*>, value: String?) { text = value } } ``` 以上的代码,我们为TextView定义了一个扩展函数TextView.provideDelegate,而这个扩展函数的返回值类型是ReadWriteProperty。通过这样的方式,我们的TextView就相当于支持了String属性的委托了。 它的使用方式也很简单: ```kotlin val textView = findViewById(R.id.textView) // ① var message: String? by textView // ② textView.text = "Hello" println(message) // ③ message = "World" println(textView.text) 结果: Hello World ``` - 在注释①处的代码,我们通过委托的方式,将message委托给了textView。这意味着,message的getter和setter都将与TextView关联到一起。 - 在注释②处,我们修改了textView的text属性,由于我们的message也委托给了textView,因此这时候,println(message)的结果也会变成“Hello”。 - 在注释③处,我们改为修改message的值,由于message的setter也委托给了textView,因此这时候,println(textView.text)的结果会跟着变成“World”。 #### 案例3:ViewModel委托 在Android当中,我们会经常用到ViewModel来存储界面数据。同时,我们不会直接创建ViewModel的实例,而对应的,我们会使用委托的方式来实现。 ```kotlin // MainActivity.kt private val mainViewModel: MainViewModel by viewModels() ``` 这一行代码虽然看起来很简单,但它背后隐藏了ViewModel复杂的实现原理。为了不偏离本节课的主题,我们先抛开ViewModel的实现原理不谈。在这里,我们专注于研究ViewModel的委托是如何实现的。 我们先来看看viewModels()是如何实现的: ```kotlin public inline fun ComponentActivity.viewModels( noinline factoryProducer: (() -> Factory)? = null ): Lazy { val factoryPromise = factoryProducer ?: { defaultViewModelProviderFactory } return ViewModelLazy(VM::class, { viewModelStore }, factoryPromise) } public interface Lazy { public val value: T public fun isInitialized(): Boolean } ``` 原来,viewModels()是Activity的一个扩展函数。也是因为这个原因,我们才可以直接在Activity当中直接调用viewModels()这个方法。 另外,我们注意到,viewModels()这个方法的返回值类型是Lazy,那么,它是如何实现委托功能的呢? ```kotlin public inline operator fun Lazy.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来动态调整委托逻辑。  看到这里,相信你也发现了,Kotlin当中看起来毫不起眼的委托,实际上它的功能是极其强大的,甚至可以说它比起扩展毫不逊色。其实,只是因为Kotlin的委托语法要比扩展更难一些,所以它的价值才更难被挖掘出来,进而也就容易被开发者所低估。 希望这节课的内容可以对你有所启发,也希望你可以将Kotlin强大的委托语法,应用到自己的工作当中去。 ### 思考题 这节课我们学习了Kotlin的委托语法,也研究了几个委托语法的使用场景,请问你还能想到哪些Kotlin委托的使用场景呢?欢迎在评论区分享你的思路,我们下节课再见。
嘿手大叔
2024年10月28日 14:56
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
分享
链接
类型
密码
更新密码