by 关键字是 Kotlin 中非常高频的设计,尤其是在 Android 开发中。它的核心思想来自一个设计模式:委托(Delegation)。而委托模式的核心思想是:一个对象把部分职责交给另一个对象去做。它是 GoF 23 个设计模式之一,实战频率极高。而为什么要其实战频率高,这主要是因为面向对象有个经典原则:组合优于继承。
不过关于设计模式,我们必须得先说明:GoF 23 个设计模式里没有"委托模式"。委托是一种编程技巧,装饰器是一种设计模式。但它们描述的是同一件事。
- 装饰器模式:关注的是意图 ------ 在不改变接口的前提下,给对象增加行为。强调的是"装饰"语义,层层包裹。
- 委托:关注的是实现手段 ------ 把方法调用转发给另一个对象。强调的是"转发"机制。
GoF 装饰器模式的实现方式就是:持有被装饰对象的引用,转发调用,在转发前后加逻辑。这个实现方式就叫委托。所以 GoF 装饰器 = 委托这个技巧 + "增加行为"的意图。
委托不只服务于装饰器,其他模式也用委托:
| 模式 | 委托的角色 | 和装饰器的区别 |
|---|---|---|
| 装饰器 | 委托对象和宿主实现同一接口,增加行为 | 增强功能 |
| 代理委 | 托对象控制访问(权限、懒加载、远程) | 控制访问 |
| 适配器 | 委托对象做接口转换 | 转换接口 |
| 外观 | 委托对象封装子系统复杂逻辑 | 简化调用 |
同样是"持有引用 + 转发调用",意图不同就是不同模式。
Kotlin 的 by 关键字提供的是委托机制(自动生成转发代码)。它本身不关心你的意图是装饰、代理还是适配 ------ 那是你通过怎么覆盖方法、选择什么委托对象来决定的。
一句话:委托是实现手段,装饰器是设计意图。Kotlin 的 by 是委托机制的语法糖,装饰器和代理是它的两种用法。

Java 中的委托
Java 中并没有对委托机制有语言层级的封装。因此,在 Java 中的委托必须持有委托对象的引用,然后逐个方法转发:
java
public interface Logger {
void log(String msg);
void error(String msg);
void debug(String msg);
}
public class ConsoleLogger implements Logger {
public void log(String msg) { System.out.println("[LOG] " + msg); }
public void error(String msg) { System.out.println("[ERR] " + msg); }
public void debug(String msg) { System.out.println("[DBG] " + msg); }
}
// 手写委托 ------ 每个方法都要转发
public class TimestampLogger implements Logger {
private final Logger delegate; // 委托对象
public TimestampLogger(Logger delegate) {
this.delegate = delegate;
}
@Override public void log(String msg) { delegate.log("[" + now() + "] " + msg); }
@Override public void error(String msg) { delegate.error("[" + now() + "] " + msg); }
@Override public void debug(String msg) { delegate.debug("[" + now() + "] " + msg); }
private String now() { return java.time.LocalTime.now().toString(); }
}
上面的代码中,TimestampLogger 只是将打 Log 的方案全部委托给了成员变量 delegate,需要在接口的每个方法中转发给 delegate 的方法。
这样的痛点很明显:接口有 N 个方法,委托类就要手写 N 个转发方法。全是模板代码,没有业务价值。接口一改,所有委托类跟着改。Logger 才 3 个方法,如果是一个 20 个方法的接口呢?
Kotlin 中的委托
我们见识了 Java 中委托的繁琐,那就看一看 Kotlin 中委托的简洁,也就是:by 关键字。
对于上面的 Java 代码,其转换为 Kotlin 代码就简单得多:
kotlin
interface Logger {
fun log(msg: String)
fun error(msg: String)
fun debug(msg: String)
}
class ConsoleLogger : Logger {
override fun log(msg: String) = println("[LOG] $msg")
override fun error(msg: String) = println("[ERR] $msg")
override fun debug(msg: String) = println("[DBG] $msg")
}
// by 关键字 ------ 编译器自动生成所有方法转发
class TimestampLogger(logger: Logger) : Logger by logger {
// 只覆盖你关心的方法,其余自动委托
override fun log(msg: String) {
println("[${java.time.LocalTime.now()}] $msg")
}
}
Logger by logger 这一行告诉编译器:TimestampLogger 实现了 Logger 接口,但我不想手写那些转发逻辑,你帮我生成 ------ 把调用全部转发给 logger 这个实例。
而这种委托方式生成的字节码和 Java 手写委托完全一致。零运行时开销,纯编译期特性。
见识完 Kotlin 中的委托,我们就来对 by 这个关键字进行全方位的了解。
by 的完整用法
一、接口委托
接口委托就是把接口的实现委托给另一个对象,像上面的 TimestampLogger 例子,就是一个典型的接口委托的示例。
这里为了加深理解,我们再给出一个接口委托的示例:
kotlin
interface Repository {
fun save(id: Int, data: String)
fun load(id: Int): String
fun delete(id: Int)
}
// 被委托对象
class MemoryRepo : Repository {
val map = mutableMapOf<Int, String>()
override fun save(id: Int, data: String) { map[id] = data }
override fun load(id: Int): String = map[id] ?: ""
override fun delete(id: Int) { map.remove(id) }
}
// 只覆盖关心的方法,其余自动转发
class LoggedRepo(private val repo: Repository) : Repository by repo {
override fun save(id: Int, data: String) {
println("before save")
repo.save(id, data)
println("after save")
}
}
这里特别注意,LoggedRepo 的构造参数是 Repository 接口,而不是具体的 MemoryRepo 类。其原因就是委托应该面向接口,不应该绑定具体实现。用 MemoryRepo 作为参数类型意味着只能传 MemoryRepo,传不了其他 Repository 实现比如 SqlRepo、RemoteRepo。装饰器模式的要点就是可以层层嵌套------LoggedRepo(SqlRepo(RemoteRepo())),其前提是参数类型是接口而不是具体类。
上面的例子只是单一接口的委托,那么如果一个类要实现多个接口呢?
二、多接口委托
对于多个接口的实现类,Kotlin 也提供了很好的委托支持,每个接口各自委托给不同的对象,互不干扰。例如下面的代码:
kotlin
interface Logger {
fun log(msg: String)
}
interface Cache {
fun get(key: String): String?
fun put(key: String, value: String)
}
interface Validator {
fun validate(input: String): Boolean
}
// 三个接口,分别委托给三个不同对象
class Service(logger: Logger, cache: Cache, validator: Validator) :
Logger by logger, Cache by cache, Validator by validator {
// 可以有自己的额外方法
fun process(input: String) {
if (!validate(input)) return // 调 Validator 的方法
put("last", input) // 调 Cache 的方法
log("processed: $input") // 调 Logger 的方法
}
}
不过使用这种多接口的委托需要注意两个事项:
- 同一个接口只能委托给一个对象。不能写 Logger by logger1, Logger by logger2------重复了,编译器不知道方法该转发给谁。
- 不能委托给冲突的接口。如果两个接口有签名相同的方法,委托给不同对象会冲突,如下:
kotlin
interface A { fun doSomething() }
interface B { fun doSomething() }
// 编译错误:doSomething 的实现冲突
class X(a: A, b: B) : A by a, B by b
如果有冲突,那么就需要手动覆盖冲突的方法,自己决定转发给谁:
kotlin
class X(a: A, b: B) : A by a, B by b {
override fun doSomething() {
// 手动决定
}
}
在 Kotlin 中,接口委托的本质就是编译器帮你写转发代码。多个接口委托给多个对象,本质也是编译器帮你生成了多个接口的转发方法。每个接口独立,互不干扰。
三、属性委托
前面的两个用法是接口委托,而 by 关键字更常用的是属性委托。也就是把属性的 getter、setter 逻辑委托给一个对象。
但是这个属性委托对于委托对象有一个要求:委托对象必须提供 getValue 和(对 var)setValue 操作符函数。
下面是一个属性委托的示例:
kotlin
// 自定义属性委托
class Validator<T>(private val initial: T, private val check: (T) -> Boolean) {
private var value = initial
operator fun getValue(thisRef: Any?, property: KProperty<*>): T = value
operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: T) {
if (!check(newValue))
throw IllegalArgumentException("${property.name} 校验失败")
value = newValue
}
}
class User {
var age: Int by Validator(0) { it in 0..150 }
var name: String by Validator("") { it.isNotBlank() }
}
fun main() {
val u = User()
u.age = 25 // OK
u.name = "张三" // OK
u.age = 200 // 抛异常
}
这里涉及到了 Kotlin 中的运算符重载。
Kotlin 运算符重载
上面的 Validator<T> 类,其中的 getValue 和 setValue 这两个方法的签名是 Kotlin 规定死的,这也是属性委托的契约协议,编译器会只认这个方法签名,而不认类名。
因此上面的这个 Validator<T> 类,除了方法名,其他都是可以自定义的。getValue 对应属性读 .,setValue 对应属性写 =。下面我们就来看一下这两个操作符函数。
kotlin
operator fun getValue(thisRef: Any?, property: KProperty<*>): T
其两个参数和返回值的含义为:
thisRef: Any?:宿主对象。属性声明在哪个类里,就是那个类的实例。顶层属性时为 null,在上面的例子中,就是当前的 User 对象property: KProperty<*>:属性的元数据。包含属性名、类型、是否可变等信息- 返回值
T:属性读取时返回的值
当调用 user.name 时,Kotlin 就会调用这个 getValue 方法,并将这个方法的返回值交给 user.name 的调用方;
kotlin
operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: T)
这个 setValue 的方法是调用 user.name = "Avril" 时会被 Kotlin 调用,其中需要被赋的新值通过第三个参数 newValue 传递过来,前两个参数与 getValue 中一样。
这两个方法的函数名、参数个数、参数顺序不能改。编译器按固定签名匹配,但是其中的泛型是可以调整的:
T可以是任意类型,和属性类型一致就行thisRef可以缩窄为具体类型,比如针对上面例子中的用法,就可以改为operator fun getValue(thisRef: User, property: KProperty<*>): T,这样这个委托只能用在User类中property的泛型可以缩窄,但一般用KProperty<*>就够了
只要按照上面的标准写法来定义这两个方法,就可以实现自定义委托。但是,Kotlin 标准库中也提供了接口让自定义委托更规范:
kotlin
public interface ReadOnlyProperty<in R, T> {
public operator fun getValue(thisRef: R, property: KProperty<*>): T
}
public interface ReadWriteProperty<in R, T> : ReadOnlyProperty<R, T> {
public operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
}
使用这两个接口,我们可以将上面例子中的自定义委托改变为如下的形式:
kotlin
class Validator<T>(private val initial: T, private val check: (T) -> Boolean)
: ReadWriteProperty<Any?, T> {
private var value = initial
override fun getValue(thisRef: Any?, property: KProperty<*>): T = value
override fun setValue(thisRef: Any?, property: KProperty<*>, newValue: T) {
if (!check(newValue)) throw IllegalArgumentException("${property.name} 校验失败")
value = newValue
}
}
不过不用这两个接口也没问题:委托协议只认签名,不认接口。但实现接口可以明确意图,也能让代码更规范。
自定义委托中的存值问题
关于属性委托,我们还有一个问题需要探讨,那就是真实的数据是存放在哪里的?是在 User.name 中,还是在 Validator<String> 中的。针对下面的代码,在经过编译后,age 和 name 是否还存在呢?
kotlin
class User {
var age: Int by Validator(0) { it in 0..150 }
var name: String by Validator("") { it.isNotBlank() }
}
答案是不存在的。age 和 name 作为成员变量,在经过委托后,会被编译为如下形式:
kotlin
public final class User {
private final Validator<Integer> age$delegate = new Validator<>(0, ...);
private final Validator<String> name$delegate = new Validator<>("", ...);
}
而我们真正存放值的地方,是 Validator 中的成员变量 value(这个 value 变量需要我们自己添加)。
而外面定义的 User.age 和 User.name,它们是属性的元数据,不是字段。是编译期的属性声明,不是运行时的字段。它们的意义是告诉编译器生成 getter、setter 并关联到对应的委托对象。KProperty<*> 里的 property.name 返回的就是这两个属性的元数据中记录的名字。而也正是 property 这个参数,让委托对象知道当前是哪个属性在读写。
kotlin
class User {
var name: String by NameDelegate()
}
// 会生成如下的代码:
class User {
private val nameDelegate = NameDelegate()
var name: String
get() = nameDelegate.getValue(this, ::name)
set(value) = nameDelegate.setValue(this, ::name, value)
}
不过准确地说,User.name 作为属性仍然存在 ------ 它有 getter 和 setter 方法,只是没有后端字段(field)。值存在委托对象内部。不存在的是 name 的后端字段,不是 name 这个属性本身。
四、标准库内置委托
除了上面说到的接口委托和属性委托,Kotlin 在其标准库中也添加了很多内置的委托供我们使用,下面我们来一一说明。
lazy ------ 懒加载
kotlin
class AppConfig {
// 首次访问时初始化,之后缓存
val database by lazy { connectDatabase() }
}
lazy 默认线程安全(LazyThreadSafetyMode.SYNCHRONIZED),可选:
SYNCHRONIZED:双重检查锁,默认PUBLICATION:允许多次初始化但只取第一个结果NONE:无线程安全,单线程用
例如下面就是配置了无线程安全参数的懒加载:
kotlin
val cache by lazy(LazyThreadSafetyMode.NONE) { mutableMapOf<String, String>() }
Delegates.observable ------ 变更监听
这就是一个简单的观察者模式,在属性值发生变更时触发回调:
kotlin
class User {
var score by Delegates.observable(initialValue = 0, onChange = object : Function3<KProperty<*>, Int, Int, Unit> {
override fun invoke(property: KProperty<*>, oldValue: Int, newValue: Int) {
Log.d("avril", "property = $property, oldValue = $oldValue, newValue = $newValue")
}
})
}
为了直观,上面给出的是没有经过优化的版本,可以很方便的看到其参数是初始值和回调方法。
经过优化后能看到其代码会变得非常简单:
kotlin
var score by Delegates.observable(0) { prop, old, new ->
println("${prop.name}: $old -> $new")
}
score = 10 // 输出: score: 0 -> 10
score = 20 // 输出: score: 10 -> 20
Delegates.vetoable ------ 变更拦截
进行赋值时,会调用检测方法,如果返回 true 才允许赋值。
kotlin
class User {
var level by Delegates.vetoable(initialValue = 1, onChange = object : Function3<KProperty<*>, Int, Int, Boolean> {
override fun invoke(property: KProperty<*>, oldValue: Int, newValue: Int): Boolean {
Log.d("avril", "property = $property, oldValue = $oldValue, newValue = $newValue")
return newValue > oldValue
}
})
}
经过优化后的代码如下:
kotlin
var level by Delegates.vetoable(1) { prop, old, new ->
new > old // 返回 false 则拒绝赋值
}
level = 5 // OK,level = 5
level = 2 // 被拒绝,level 仍为 5
Delegates.notNull ------ 延迟赋值非空
当使用此委托时,需要手动赋值,如果被委托数据为 null 时进行访问会抛出异常:
kotlin
var id: String by Delegates.notNull()
// 此时访问 id 会抛异常
id = "abc" // 赋值后可正常访问
这个委托其就是很简单的自定义委托,只是在 getValue 方法中判断了一下并做了异常抛出:
kotlin
private class NotNullVar<T : Any>() : ReadWriteProperty<Any?, T> {
private var value: T? = null
public override fun getValue(thisRef: Any?, property: KProperty<*>): T {
return value ?: throw IllegalStateException("Property ${property.name} should be initialized before get.")
}
public override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
this.value = value
}
public override fun toString(): String =
"NotNullProperty(${if (value != null) "value=$value" else "value not initialized yet"})"
}
map 委托 ------ 从 Map 读写属性
使用此委托的属性,其值是存放在 map 中的:
kotlin
class User(map: Map<String, Any?>) {
val name: String by map
val age: Int by map
}
val u = User(mapOf("name" to "张三", "age" to 25))
println(u.name) // 张三
不过使用这种 map 委托的话,需要注意 map 的类型和被委托的属性的可变性。因为 Map 委托只提供 getValue 只读属性,因此只能委托给 val 属性,针对 var 属性,需要使用 MutableMap 这种同时提供 getValue 和 setValue 方法的接口。
例如,下面就是针对 var 的属性的委托:
kotlin
val map : MutableMap<String, Any> = mutableMapOf()
var arg1: String by map
另外需要注意的是,由于 map 委托是通过字段名在 map 中查找对应的值的,因此 Map 中的键的类型必须是 String。
五、provideDelegate ------ 委托工厂
前面自定义属性委托中,by 表达式右侧的对象就是委托对象。但 Kotlin 还提供了一层中间机制:provideDelegate。它的作用是在属性初始化时做一次拦截,返回真正的委托对象。
这里我们用一个简单的委托对象和一个简单的委托工厂类来演示:
kotlin
// 委托对象
class Validator<T> (initial: T) {
private var value = initial
operator fun getValue(thisRef: Any?, property: KProperty<*>): T = value
operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: T) {
value = newValue
}
}
// 委托工厂,创建委托对象
class DelegateProvider<T>(private val value: T) {
// 属性初始化时调用一次,早于第一次 getValue
operator fun provideDelegate(thisRef: Any?, property: KProperty<*>) : Validator<T> {
return Validator<T>(value) // 返回真正的委托对象
}
}
class User {
// 将 param1 委托给工厂
var param1: Int by DelegateProvider(10)
}
provideDelegate 的调用时机:属性声明所在的类被实例化时,为每个属性调用一次。早于第一次 getValue。因此 provideDelegate 的就可以用来做这些事情:权限校验、属性元数据检查、根据上下文返回不同委托对象。我们上面说的,标准库中的 Delegates.observable、vetoable、notNull 底层都通过 provideDelegate 实现。
但是你不写 provideDelegate 也完全没问题,by 表达式右侧的对象直接作为委托对象。写了就多一层拦截,仅此而已。
六、委托给另一个属性
见识了上面的委托之后,这个就好理解了。就是将一个属性委托给另一个属性:
kotlin
// 把 topName 委托给 realName
class Person {
val realName = "张三"
val topName by this::realName // 引用另一个属性
}
fun main() {
val p = Person()
println(p.topName) // 张三
}
这种用法的情况非常少,仅做一下了解即可。
七、委托可用于局部变量和顶层属性
上面的例子都是使用类成员进行委托,但委托对象不限于类成员,函数内的局部变量也能用:
kotlin
fun main() {
var count by Delegates.observable(0) { _, old, new ->
println("$old -> $new")
}
count = 1 // 输出: 0 -> 1
count = 2 // 输出: 1 -> 2
}
局部变量委托在编译后,委托对象存在函数的栈帧中,生命周期随函数调用结束而结束。
不仅是局部变量,属性声明在文件顶层(不在任何类内),也能使用委托:
kotlin
// 顶层属性
val globalConfig: String by lazy { loadConfigFromFile() }
fun main() {
println(globalConfig) // 首次访问时初始化
}
此时 getValue 的 thisRef 参数为 null,因为属性不属于任何对象。
八、单例对象委托
前面的例子中,每个属性都拥有自己的委托对象,我们可以用 object 声明的单例对象作为委托对象,这样就可以让多个属性共享同一个委托实例:
kotlin
object LogDelegate {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "${property.name} accessed at ${System.currentTimeMillis()}"
}
}
class Server {
val status: String by LogDelegate
val health: String by LogDelegate
}
但是使用这种委托要格外小心:单例对象全局唯一,如果委托对象内部存值(如 Validator 那样),多个属性会共享同一个存储字段导致冲突。因此,单例委托适合无状态的委托(只做日志、计算等,不存值)。
九、限制:扩展属性不能用 by
Kotlin 的扩展属性没有后端字段,而属性委托需要存储空间(存委托对象)。因此扩展属性不能使用 by:
kotlin
// ❌ 编译错误:扩展属性不能用委托
val String.isLongEnough: Boolean by lazy { this.length > 10 }
如果需要对扩展属性做类似逻辑,只能用普通 getter:
kotlin
val String.isLongEnough: Boolean
get() = this.length > 10
其实,扩展属性最好不要将其理解为属性,你可以将其理解为扩展方法比较好。
普通属性有后端字段,因为属性声明在类内部,类实例上空间有这个字段。扩展属性声明在类外部,它不属于任何类的实例。你没法往 String 类里加字段------String 是 JDK 的类,你改不了它的内存布局。所以扩展属性只能定义 getter/setter 逻辑,不能存值。
实际开发中,var 扩展属性几乎没用(setter 存不了值没意义)。val 扩展属性有点用,就是把常用计算封装成属性访问语法。每一次访问这个属性时,就会重新进行一次计算。
简单总结就是,扩展属性不是真正的属性------没有后端字段,不能存值,本质上是披着属性外衣的扩展函数。val 扩展属性等价于一个无参扩展函数,var 扩展属性等价于两个扩展函数(getter + setter),但 setter 因为存不了值所以基本没用。
另外扩展属性这个特性不是特别好,很容易带来兼容性问题,最好不要用这个特性。

总结
在 Java 中,委托意味着手写大量转发代码------接口一改,所有委托类跟着改。Kotlin 用 by 一个关键字解决了这个问题,编译期展开,零运行时开销。用一句话概括它的价值那就是:把编译器该干的活还给编译器。这就是 Kotlin 对委托的语言级支持。
在上面的内容中,我们对 by 进行了全方位的解析。其中最核心的要点为:属性委托后,宿主类没有后端字段,值存在委托对象内部。
这是 by 的设计决定 ------ 读写行为和值存储绑在一起交给委托对象,使用统一的 getValue、setValue 运算符重载协议。这个协议是 Kotlin 委托(无论 lazy、map、notNull 还是自定义)都需要遵守的一致性设计。