如果你还没有搞懂 Kotlin 委托属性,进来看看

委托属性 让你可以把属性的 gettersetter 逻辑交给另一个对象处理。

通过 by 关键字,属性会连接到一个委托对象,由它来定义属性如何存储和读取。

如何工作

正如上面提到的那样,委托属性把 gettersetter 委托给另一个对象(称为委托)。

委托负责管理属性的值,并提供自定义的访问或修改逻辑。

好的,接下来为了加深理解,我来举一些常见的使用场景:

延迟初始化 lazy

lazy 委托让属性只在首次访问时才初始化,避免对象创建时就进行初始化:

kotlin 复制代码
val lazyValue: String by lazy {
    println("Computed!")
    "Hello, Kotlin!"
}

这段代码中,"Computed!" 只会在第一次访问 lazyValue 时打印,值 "Hello, Kotlin!" 也只在此时被赋值。

可观察属性 observable

这个委托让你能在值变化时触发回调:

kotlin 复制代码
import kotlin.properties.Delegates

var observableValue: String by Delegates.observable("Initial value") { _, old, new ->
    println("Value changed from $old to $new")
}
observableValue = "New value"
// 输出: Value changed from Initial value to New value

可否决属性 vetoable

vetoableobservable 类似,但可以根据条件否决更改:

kotlin 复制代码
var vetoableValue: Int by Delegates.vetoable(0) { _, old, new ->
    new > old // 只有新值大于旧值才接受
}
vetoableValue = 5 // 允许
vetoableValue = 2 // 被否决,值不变

Map 委托

这种方式适合动态属性,属性名和值都存在 Map 里:

kotlin 复制代码
class User(val map: Map<String, Any?>) {
    val name: String by map
    val age: Int by map
}

val user = User(mapOf("name" to "John Doe", "age" to 30))
println(user.name) // John Doe
println(user.age)  // 30

你会发现,你无需写任何额外的代码,Kotlin 天然就给你了 map 委托的语法糖。

局部委托属性

KEEP 提案 Local delegated properties 引入了局部委托属性,允许在函数或代码块内声明的属性使用委托。

这让局部变量也能把行为委托给自定义委托,增强了代码的可重用性和封装性,同时保持了与全局和类级委托的一致性。

kotlin 复制代码
import kotlin.reflect.KProperty

class Delegate {
    operator fun getValue(t: Any?, p: KProperty<*>): Int = 1
}

fun box(): String {
    val prop: Int by Delegate()
    return if (prop == 1) "OK" else "fail"
}

个人觉得这个功能算是个面子工程,至少统一了成员变量和普通变量的行为,实际意义并不大。

总结

委托属性提供了一种优雅的方式,把属性访问委托给其他对象来实现特定行为------延迟初始化、变更观察、条件否决或映射属性值。这让代码更简洁、更模块化,属性管理逻辑可以和其他代码清晰分离。

进阶:Lazy 的内部机制

Kotlin 的 lazy 委托非常实用。它创建的属性只在首次访问时计算值,然后缓存起来供后续调用使用。表面上看 API 很简单,但深入源码你会发现一个设计精良的系统------基于 Lazy 接口,提供了多种专门实现来处理不同的线程安全需求。

Lazy 接口

整个 lazy 机制围绕一个简洁的接口构建:

kotlin 复制代码
public interface Lazy<out T> {
    /**
     * 获取延迟初始化的值。
     * 一旦初始化完成,值在 Lazy 实例的整个生命周期内不会改变。
     */
    public val value: T

    /**
     * 判断值是否已初始化。
     * 一旦返回 true,就会一直保持 true。
     */
    public fun isInitialized(): Boolean
}
  • value: T:只读属性,是主要入口。首次访问时触发初始化器 lambda 执行,后续访问直接返回缓存结果。
  • isInitialized(): Boolean:让你检查初始化器是否已运行,而不会触发它。

属性委托的关键在于 getValue 操作符扩展函数:

kotlin 复制代码
@kotlin.internal.InlineOnly
public inline operator fun <T> Lazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value

它简单地把属性读取委托给 lazy.value

内部状态管理

从内部看,所有的 Lazy 实现都采用相同的状态跟踪策略:使用一个特殊的单例对象 UNINITIALIZED_VALUE 作为内部标记。私有的 _value 字段初始化为这个标记。访问 value 时,检查 _value 是否 === UNINITIALIZED_VALUE------是则运行初始化器,否则返回缓存值。

初始化器运行后会发生两件事:

  1. _value 更新为计算结果
  2. 初始化器 lambda 的引用置为 null,允许垃圾回收,防止内存泄漏

三种实现模式

如果你好奇 lazy 的实现,那么你会看到下面这样的代码:

Kotlin 复制代码
public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
    when (mode) {
        LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
        LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
        LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
    }

lazy 函数是一个工厂,根据 LazyThreadSafetyMode 返回三种不同的内部实现:

1. NONE

最简单、最快,但最不安全:

kotlin 复制代码
override val value: T
    get() {
        if (_value === UNINITIALIZED_VALUE) {
            _value = initializer!!()
            initializer = null
        }
        return _value as T
    }

机制:简单的非同步检查。首次访问时运行初始化器并存储结果。

使用场景 :完全不提供线程安全。如果两个线程同时访问未初始化的实例,初始化器可能被调用两次。多线程环境下行为未定义。只有当你能保证 lazy 属性只从单线程初始化和访问时才使用。因为没有同步开销,性能最佳。

2. SYNCHRONIZED

默认实现,最健壮,保证即使在高度并发环境下初始化器也只执行一次:

kotlin 复制代码
override val value: T
    get() {
        if (_value !== UNINITIALIZED_VALUE) {
            return _value as T
        }

        return synchronized(lock) {
            if (_value !== UNINITIALIZED_VALUE) {
                _value as T
            } else {
                val typedValue = initializer!!()
                _value = typedValue
                initializer = null
                typedValue
            }
        }
    }

机制:经典的双重检查锁定模式。

  1. 第一次检查(无锁) :先在同步块外检查 _value。如果已初始化,直接返回,省去获取锁的开销。这让首次访问后的后续访问非常快。
  2. 第二次检查(带锁) :如果未初始化,进入 synchronized(lock) 块。在锁内再次检查 _value。这第二次检查很关键------处理了竞争条件:另一个线程可能在第一次检查和当前线程获取锁之间完成了初始化。
  3. 初始化:只有值仍未初始化时,当前线程才执行初始化器 lambda 并存储结果。

使用场景 :默认模式,是任何可能被多线程访问的属性的最安全选择。保证初始化的原子性和结果在所有线程中的可见性(得益于 synchronized 的内存保证和 _value 上的 volatile 注解)。

3. PUBLICATION

提供更宽松的线程安全形式:保证只使用一个最终值,但允许初始化器被调用多次:

kotlin 复制代码
override val value: T
    get() {
        if (_value !== UNINITIALIZED_VALUE) {
            return _value as T
        }

        val initializerValue = initializer
        if (initializerValue != null) {
            val newValue = initializerValue()
            // 尝试原子设置值
            if (valueUpdater.compareAndSet(this, UNINITIALIZED_VALUE, newValue)) {
                initializer = null
                return newValue
            }
        }
        return _value as T
    }

机制 :使用 AtomicReferenceFieldUpdater 的无锁方法。

  1. 多个线程可以同时访问未初始化的值并并发调用 initializerValue() lambda,竞争成为第一个设置值的线程。
  2. compareAndSet (CAS) 是原子操作。只有当 _value 仍然是 UNINITIALIZED_VALUE 时,才会成功设置为 newValue
  3. 只有一个线程会"赢得"竞争,它的 newValue 成为最终值。其他计算了值的线程会丢弃结果,使用胜出者的值。

使用场景:当初始化器是廉价的幂等操作(可以安全多次调用)且你想避免锁的潜在竞争时很有用。它用可能的冗余计算换取无锁并发。

进阶:lazy 的 Java 字节码

Kotlin 的 by lazy { ... } 是个巧妙的性能优化------把昂贵的计算推迟到实际需要时才执行。语法简洁,但 Kotlin 编译器做了特定的转换来实现这个行为。

从一个简单例子开始:

kotlin 复制代码
class UserSession {
    val heavyUserData: String by lazy {
        println("Computing heavy user data...")
        Thread.sleep(1000)
        "User Profile Data"
    }
}

"Computing heavy user data..." 只在首次访问 heavyUserData 时打印。编译后反编译为 Java,lazy 属性被转换成几个组件:

java 复制代码
public final class UserSession {
    // 1. 保存 Lazy 实例的私有 final 字段
    @NotNull
    private final Lazy heavyUserData$delegate;

    // 2. 属性的静态元数据字段
    static final /* synthetic */ KProperty<Object>[] $$delegatedProperties =
        new KProperty[] { (KProperty) new PropertyReference0Impl(
            UserSession.class, "heavyUserData",
            "getHeavyUserData()Ljava/lang/String;") };

    // 3. 公共 getter 方法
    @NotNull
    public final String getHeavyUserData() {
        return (String) this.heavyUserData$delegate.getValue(this, $$delegatedProperties[0]);
    }

    public UserSession() {
        // Lazy 实例在构造函数中创建
        this.heavyUserData$delegate = LazyKt.lazy((Function0) new Function0<String>() {
            @NotNull
            public final String invoke() {
                System.out.println("Computing heavy user data...");
                Thread.sleep(1000L);
                return "User Profile Data";
            }
        });
    }
}

三个关键组件

1. Lazy 委托字段

编译器不会创建名为 heavyUserData 的字段来存 String 值,而是创建一个带 $delegate 后缀的私有 final 字段:

java 复制代码
private final Lazy heavyUserData$delegate;

类型是 kotlin.Lazy,用来保存延迟初始化器对象实例(比如 SynchronizedLazyImpl)。

构造函数中,这个字段通过 LazyKt.lazy(...) 初始化。你提供的 lambda 被编译成匿名 Function0 类传给 lazy 工厂函数,SynchronizedLazyImpl(或其他模式)就在这里创建和存储。

2. KProperty 元数据字段

这个静态数组支持反射功能:

java 复制代码
static final KProperty<Object>[] $$delegatedProperties

包含委托属性的元数据:名称(heavyUserData)、所有者(UserSession.class)、getter 签名。

这让 getValue 操作符函数知道正在访问哪个属性。对于简单的 lazy 委托,主要是满足 getValue 函数签名要求。

3. 公共 Getter 方法

这是属性的公共访问点。Kotlin 中写 session.heavyUserData,实际调用的就是这个方法:

java 复制代码
public final String getHeavyUserData() {
    return (String) this.heavyUserData$delegate.getValue(this, $$delegatedProperties[0]);
}

getter 不返回简单字段,而是委托给 heavyUserData$delegateLazy 实例的 getValue 操作符函数。

如前所述,Lazy<T>.getValue(...) 是核心逻辑所在------首次调用时执行初始化器 lambda、缓存结果并返回,后续调用直接返回缓存值。

具体一点,如果以默认实现 SYNCHRONIZED 为例,当我们调用属性的 getter 时,实际上是调用的 SynchronizedLazyImpl 实现中的 getValue 方法。

这就是 lazy 行为的实现原理:实际计算只发生在 getValue 调用内部,而 getValue 只在首次调用 getter 时触发。

没什么魔法,只是 Kotlin 编译器在背后做了更多工作。


如果学习了委托,那么就不得不提到 Kotlin 的另一个特性:幕后字段和幕后属性。

幕后字段和幕后属性

幕后字段(Backing Fields)和幕后属性(Backing Properties)都是用来管理属性值并提供受控访问的机制。虽然用途相似,但实现方式和适用场景各有不同。

幕后字段

当你为属性定义自定义 gettersetter 并使用 field 关键字时,Kotlin 会自动生成一个幕后字段。这是一个隐式的存储机制,让属性既能保存值,又能执行自定义的访问或修改逻辑。

看个例子:

kotlin 复制代码
var name: String = "Default"
    get() = field.uppercase() // 使用幕后字段的自定义 getter
    set(value) {
        field = value.trim() // 使用幕后字段的自定义 setter
    }

这里的 field 就是 name 属性的幕后字段。Kotlin 会把属性值存到 field 里,然后通过你定义的 gettersetter 来读写它。

幕后属性

幕后属性则是你显式定义的一个变量,专门用来存储属性的实际值。和幕后字段不同,幕后属性需要手动创建,这让你能完全掌控属性的内部表示。

当你需要更高级的自定义时,这种方式就派上用场了------比如把实际存储私有化,同时对外暴露一个计算属性:

kotlin 复制代码
private var _age: Int = 0 // 幕后属性,存储实际值
var age: Int
    get() = _age
    set(value) {
        if (value >= 0) _age = value // 自定义验证逻辑
    }

这个例子中,_age 是私有的幕后属性,负责管理 age 属性的实际存储。公共的 age 属性通过自定义逻辑访问 _age,实现了存储的封装。

主要区别

两者的核心差异在于实现方式和灵活性:

幕后字段 由 Kotlin 隐式提供,只能在属性的 gettersetter 内部通过 field 关键字使用。它和属性绑定在一起,适合简单场景。

幕后属性需要显式声明,提供了更灵活的属性行为处理方式。它支持更复杂的逻辑和自定义存储机制,是高级自定义场景的理想选择。

实际上,这里的区分有一个诀窍------幕后属性一定会创建一个新的变量去存储信息!

总结

幕后字段和幕后属性听起来很像,都是用来管理属性值的受控访问。区别在于:幕后字段是隐式的,直接和属性绑定,适合简单的自定义逻辑;幕后属性是显式定义的,把存储和属性本身解耦,提供了更大的灵活性。

理解什么时候用哪种机制,能帮你写出更健壮、更易维护的 Kotlin 代码。

相关推荐
黄林晴2 小时前
苦等多年!Compose 终于迎来原生 Media3 播放器
android
亘元有量-流量变现2 小时前
深度技术对比:Android、iOS、鸿蒙(HarmonyOS)权限管理全解析
android·ios·harmonyos·方糖试玩
米码收割机2 小时前
【Android】基于安卓app的健身房会员管理系统(源码+部署方式+论文)[独一无二]
android
酿情师2 小时前
2026软件系统安全赛初赛RSA(赛后复盘)
android·网络·安全·密码学·rsa
Digitally2 小时前
如何轻松地使用隔空投送将iPhone内容传输到Android
android·ios·iphone
lishutong10062 小时前
Android 性能诊断 V2:基于 Agent Skill 的原生 IDE 融合架构
android·ide·架构
恋猫de小郭2 小时前
AGP 9.2 开始,Android 上协程启动和取消速度提升两倍
android·前端·flutter
devlei10 小时前
从源码泄露看AI Agent未来:深度对比Claude Code原生实现与OpenClaw开源方案
android·前端·后端
阿拉斯攀登13 小时前
从入门到实战:CMake 与 Android JNI/NDK 开发全解析
android·linux·c++·yolo·cmake