如果你还没有搞懂 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 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台2 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone2 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc2 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo2 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077002 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
ai2work2 天前
ch23 综合复刻:从零做一个最小可用版本(capstone)
kotlin
其实防守也摸鱼2 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
ai2work2 天前
ch21 签名、校验与发版
kotlin
AFinalStone2 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui