14、委托:类委托、属性委托、标准委托(lazy、observable、vetoable)

委托模式,说白了就是「我懒得干,你替我干」。在 Kotlin 里,这个模式被提升到了语言层面,用关键字 by 就能搞定。我个人觉得,这是 Kotlin 最实用的语法糖之一,能帮你省掉大量样板代码。

今天咱们就把它拆开揉碎了讲清楚。分成三块:类委托、属性委托、还有 Kotlin 标准库自带的几个委托工具。

14.1 类委托:把实现甩给「别人」

先看一个场景。你有一个接口,比如 Logger

interface Logger {
    fun log(message: String)
    fun logError(message: String)
}

你写了一个 ConsoleLogger 实现了它。现在你想加一个 FileLogger,但大部分逻辑和 ConsoleLogger 一样,只是输出方式不同。传统做法是写一个代理类,手动转发所有方法——太累了。

Kotlin 的类委托就是干这个的:

class FileLogger(private val consoleLogger: ConsoleLogger) : Logger by consoleLogger {
    override fun logError(message: String) {
        // 只覆盖这一个方法
        println("写入文件: $message")
    }
}

看到 by consoleLogger 了吗?意思是:除了你显式 override 的方法,其他所有 Logger 接口的方法,都交给 consoleLogger 去执行。

我的习惯:类委托特别适合「装饰器模式」或「代理模式」。我在项目中重构过一个网络请求库,用类委托把日志、缓存、重试逻辑一层层包起来,代码量直接砍半。

14.2 属性委托:把读写权交出去

属性委托更常用。语法是 val/var 属性名 by 委托对象。委托对象需要实现 getValuesetValue 操作符。

举个例子,你有一个 User 类,里面的 name 属性想从数据库或 SharedPreferences 里读取:

class User {
    var name: String by PreferenceDelegate("name", "默认名字")
}

这个 PreferenceDelegate 长什么样?

class PreferenceDelegate<T>(private val key: String, private val default: T) {
    private val prefs = getSharedPreferences() // 假设有这个方法

    operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
        // 从 prefs 读取
        return prefs.getString(key, default as String) as T
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        prefs.edit().putString(key, value as String).apply()
    }
}

嗯,这里要注意:getValuesetValue 是操作符重载,必须用 operator 关键字。而且 KProperty 参数能让你拿到属性名、类型等元信息,非常强大。

核心理解:属性委托的本质是「把属性的 get/set 逻辑抽离到另一个对象」。你想想看,这样你的类就只关注业务,数据存取、校验、缓存这些脏活累活全扔给委托。

14.3 标准委托:Kotlin 自带的三个神器

Kotlin 标准库提供了三个非常实用的委托:lazyobservablevetoable。我几乎每个项目都会用到它们。

14.3.1 lazy:懒加载,用的时候才创建

这个最简单,也最常用。比如你有一个很重的对象,不想在类初始化时就创建:

val heavyObject: HeavyObject by lazy {
    println("第一次访问时才会执行这里")
    HeavyObject()
}

第一次访问 heavyObject 时,lazy 后面的 lambda 才会执行。之后访问,直接返回缓存的结果。

lazy 还有三个模式:

模式 说明 适用场景
LazyThreadSafetyMode.SYNCHRONIZED 线程安全,默认模式 多线程环境
LazyThreadSafetyMode.NONE 不加锁,性能最高 单线程或确定不会并发
LazyThreadSafetyMode.PUBLICATION 允许并发初始化,但只取第一个结果 对结果一致性要求不高
我踩过的坑:曾经在 Android 的 ViewModel 里用 lazy 加载一个网络请求结果,结果因为线程安全问题,两个线程同时触发了初始化,导致请求发了两次。后来改成 SYNCHRONIZED 才解决。所以,除非你确定是单线程,否则别用 NONE

14.3.2 observable:监听属性变化

你想在属性被赋值时干点额外的事,比如更新 UI 或打印日志:

var name: String by Delegates.observable("初始值") { property, oldValue, newValue ->
    println("属性 ${property.name} 从 $oldValue 变成了 $newValue")
}

每次 name 被赋值,lambda 都会执行。注意,observable 是在赋值之后回调,你无法阻止这次赋值。

我在项目中用它来做数据绑定。比如一个用户信息页面,name 变了,自动刷新对应的 TextView:

var userName: String by Delegates.observable("") { _, _, newValue ->
    nameTextView.text = newValue
}

省掉了手动调 updateUI() 的麻烦。

14.3.3 vetoable:带否决权的监听

这个和 observable 很像,但多了一个「否决」能力。回调返回 true 表示允许赋值,false 表示拒绝:

var age: Int by Delegates.vetoable(0) { _, _, newValue ->
    // 年龄不能为负数
    newValue >= 0
}

试试看:

age = 25  // 允许,age 变成 25
age = -5  // 拒绝,age 还是 25
注意:vetoable 的回调是在赋值之前执行的。如果返回 false,新值根本不会写入。这和 observable 的执行时机正好相反。

我曾经用 vetoable 做过一个表单校验。用户输入手机号时,只允许数字,超过 11 位就拒绝:

var phone: String by Delegates.vetoable("") { _, _, newValue ->
    newValue.all { it.isDigit() } && newValue.length <= 11
}

你看,代码简洁又直观。

14.4 知识体系总览

为了让你更直观地理解委托的整体结构,我画了一张图:

Kotlin 委托知识体系 类委托 属性委托 标准委托 by 关键字 接口实现转发 装饰器/代理模式 getValue / setValue 操作符重载 KProperty 元信息 lazy / observable vetoable 标准库内置 核心思想:把「怎么做」和「做什么」分离 类委托 → 实现复用 属性委托 → 读写逻辑抽离 标准委托 → 懒加载、监听、校验开箱即用

14.5 总结

委托这个东西,说白了就是「把活外包」。Kotlin 用 by 关键字把这个模式做得极其优雅。

  • 类委托:适合接口实现复用,避免手写转发代码。
  • 属性委托:适合把属性的存取逻辑抽离,比如 SharedPreferences、数据库字段。
  • 标准委托lazy 做懒加载,observable 做监听,vetoable 做带否决的监听。

我个人建议,刚开始用的时候,先从 lazy 入手,它最直观。然后试试 observable 做 UI 绑定。等你熟悉了,再自己写自定义委托,你会发现很多重复代码都能被它干掉。

嗯,委托就讲到这里。记住一句话:能用 by 解决的问题,就别自己手写。


公众号:蓝海资料掘金营,微信deep3321