Kotlin委托机制详解

核心思想:把事情交给别人做(代理)

想象一下:你(一个类 A)需要完成一项任务(实现一个接口 I 或者管理一个属性 p 的读取/写入逻辑),但你不想亲自做所有细节。你可以找一个助手(委托对象 delegate),把这项任务委托给它去完成。

Kotlin 通过关键字 by 在语言层面优雅地支持了两种主要的委托模式:

  1. 类委托: 把实现接口的责任委托给另一个对象。
  2. 属性委托: 把属性的读取 (get()) 和写入 (set()) 逻辑委托给另一个对象。

一、类委托:让别人替你干活(实现接口)

场景: 你需要实现一个接口 Printer,但你发现已经有一个现成的类 LaserPrinter 完美实现了这个接口。你不想重新写一遍代码,也不想直接继承 LaserPrinter(可能因为 Kotlin 是单继承,或者 LaserPrinterfinal 类)。

如何用 (by):

kotlin 复制代码
// 1. 定义一个接口
interface Printer {
    fun print(message: String)
}

// 2. 一个已经实现了该接口的类
class LaserPrinter : Printer {
    override fun print(message: String) {
        println("激光打印机滋滋滋...打印:$message")
    }
}

// 3. 你自己的类,通过 `by` 将 Printer 接口的实现委托给 laserPrinter 对象
class OfficePrinter(private val laserPrinter: LaserPrinter) : Printer by laserPrinter {
    // 你可以选择什么都不写,OfficePrinter 就有了 print 方法
    // 或者,你可以覆盖/添加自己的方法
    fun copyDocument(document: String) {
        println("正在复印:$document")
        print(document) // 调用委托的 print 方法
    }
}

// 使用
fun main() {
    val laser = LaserPrinter()
    val officePrinter = OfficePrinter(laser)

    officePrinter.print("季度报告") // 输出:激光打印机滋滋滋...打印:季度报告
    officePrinter.copyDocument("合同") // 输出:正在复印:合同 \n 激光打印机滋滋滋...打印:合同
}
  • OfficePrinter : Printer by laserPrinter: 这行是关键。它告诉编译器:

    • OfficePrinter 实现了 Printer 接口。
    • 但是 ,所有 Printer 接口方法的具体实现 ,都交给构造函数传入的 laserPrinter 对象去处理。
  • main 中调用 officePrinter.print(...) 时,实际上调用的就是 laserPrinter.print(...)

原理 (编译器魔法):

编译器在背后帮你生成了"胶水代码"。想象一下它大致为你生成了这样的代码:

kotlin 复制代码
class OfficePrinter(private val laserPrinter: LaserPrinter) : Printer {
    // 编译器自动为每个 Printer 接口的方法生成实现,并转发给 delegate
    override fun print(message: String) {
        laserPrinter.print(message) // 核心:转发调用!
    }

    // ... 你自己的 copyDocument 方法 ...
}

优点:

  1. 避免继承限制: Kotlin 单继承,委托可以让你"复用"多个类的功能。
  2. 解耦: OfficePrinter 不需要知道 LaserPrinter 内部怎么实现 print,它只依赖接口。
  3. 灵活: 可以轻松替换不同的委托对象(比如换成 InkjetPrinter),只要它们实现了相同的接口。
  4. 控制: 你可以在委托类中覆盖部分方法,添加自己的逻辑,或者只委托部分方法。

二、属性委托:让别人管你的"钱袋子"(属性存取逻辑)

场景: 你有一个属性,但它的获取 (get()) 或设置 (set()) 逻辑可能很复杂(比如延迟初始化、数据校验、日志记录、存储到数据库/SharedPreferences、监听变化等)。你想把这套逻辑抽出来复用,避免在每个属性里写重复代码。

如何用 (by) + 标准委托例子:

Kotlin 标准库提供了几个开箱即用的属性委托:

  1. lazy: 延迟初始化 (最常用!)

    kotlin 复制代码
    class MyActivity : AppCompatActivity() {
        // 第一次访问 expensiveView 时才会执行 lambda 初始化它
        private val expensiveView: MyCustomView by lazy {
            println("正在初始化昂贵的视图...")
            findViewById(R.id.expensive_view) // 假设这个 findViewById 开销很大
        }
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            // 第一次访问时初始化
            expensiveView.doSomething() // 输出:"正在初始化昂贵的视图..." 然后执行 doSomething
            // 后续再访问 expensiveView,直接返回初始化好的结果,不会再次执行 lambda
        }
    }
    • by lazy { ... }: 委托给 Lazy<T> 的一个实例。
    • 原理: Lambda 只在第一次读取 该属性时执行一次,结果被缓存。后续所有读取都直接返回缓存的值。默认线程安全 (LazyThreadSafetyMode.SYNCHRONIZED)。
  2. observable: 监听属性变化

    kotlin 复制代码
    class User {
        var name: String by Delegates.observable("<无名>") { property, oldValue, newValue ->
            println("属性 `${property.name}` 从 `$oldValue` 变成了 `$newValue`")
        }
    }
    
    fun main() {
        val user = User()
        user.name = "Alice" // 输出:属性 `name` 从 `<无名>` 变成了 `Alice`
        user.name = "Bob"   // 输出:属性 `name` 从 `Alice` 变成了 `Bob`
    }
    • by Delegates.observable(initialValue) { ... }: 委托给一个特殊的 ReadWriteProperty 对象。
    • 原理: 每当通过 = 给属性赋值时,在赋值之后,会自动调用你提供的 lambda,告诉你属性名、旧值、新值。
  3. vetoable: 拦截属性赋值 (类似 observable,但在赋值前调用,可以否决)

  4. notNull: 提供非空检查 (早期常用,现在直接用 lateinit var 更普遍)

  5. Map 委托: 属性值存储在 Map 中 (常用于解析 JSON 或配置)

    kotlin 复制代码
    data class Config(val map: Map<String, Any?>) {
        val serverUrl: String by map // 从 map 中根据属性名 "serverUrl" 取 String 值
        val port: Int by map        // 从 map 中根据属性名 "port" 取 Int 值
        val debugMode: Boolean by map
    }
    
    fun main() {
        val configMap = mapOf(
            "serverUrl" to "https://api.example.com",
            "port" to 8080,
            "debugMode" to true
        )
        val config = Config(configMap)
    
        println(config.serverUrl) // 输出:https://api.example.com
        println(config.port)      // 输出:8080
    }

自定义属性委托:

当标准库不满足需求时,你可以自己造轮子。核心是遵循约定:

  1. 只读属性 (val): 委托类需要提供一个 operator fun getValue(thisRef: R, property: KProperty<*>): T 函数。
  2. 读写属性 (var): 委托类除了 getValue,还需要提供一个 operator fun setValue(thisRef: R, property: KProperty<*>, value: T) 函数。
  • thisRef: 包含该属性的对象的引用(如果是扩展属性,则是接收者类型)。类型 R 必须是属性所有者的类型或其超类型。
  • property: 被委托的属性本身的元信息(如名称)。类型是 KProperty<*> 或其超类型。
  • value: 要设置的新值(仅在 setValue 中)。类型 T 必须与属性类型相同或其超类型。

例子:一个简单的日志记录委托

kotlin 复制代码
class LoggingDelegate<T>(private var value: T) {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
        println("读取属性 `${property.name}` = $value")
        return value
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: T) {
        println("设置属性 `${property.name}`: $value -> $newValue")
        value = newValue
    }
}

class User {
    var age: Int by LoggingDelegate(0)
}

fun main() {
    val user = User()
    user.age = 25 // 输出:设置属性 `age`: 0 -> 25
    println(user.age) // 输出:读取属性 `age` = 25 \n 25
}

属性委托原理 (编译器魔法):

当你写 val/var someProperty: Type by MyDelegate() 时,编译器大致做以下事情:

  1. 创建一个委托对象的实例: private val myDelegate$delegate = MyDelegate() (名字会修饰以避免冲突)。

  2. 为属性生成访问器:

    • 对于 val (只读):

      kotlin 复制代码
      fun getSomeProperty(): Type {
          return myDelegate$delegate.getValue(this, this::someProperty)
      }
    • 对于 var (读写):

      kotlin 复制代码
      fun getSomeProperty(): Type {
          return myDelegate$delegate.getValue(this, this::someProperty)
      }
      fun setSomeProperty(value: Type) {
          myDelegate$delegate.setValue(this, this::someProperty, value)
      }

源码调用追踪 (以 lazy 为例,简化版):

  1. val myVar: T by lazy { initializer }

  2. 编译器生成一个 Lazy 类型的委托属性实例:private val myVar$delegate = LazyKt.lazy(initializer)LazyKt.lazy() 是工厂函数。

  3. 常见的 lazy 实现是 SynchronizedLazyImpl

  4. 当你第一次访问 myVar

    • 编译器生成的 getMyVar() 被调用:return myVar$delegate.getValue(this, this::myVar)

    • SynchronizedLazyImpl.getValue(...) 被调用:

      • 检查内部标记 _value (初始是 UNINITIALIZED_VALUE)。
      • 如果是未初始化,加锁 (保证线程安全),执行 initializer lambda,将结果存储在 _value 中,并标记为已初始化,释放锁。
      • 返回 _value 存储的值。
  5. 后续访问直接返回存储的值。


总结:Kotlin 委托的精髓

  1. by 关键字: 这是委托的标记。

  2. 类委托 (by + 对象):

    • 目的:将接口实现的责任转交给另一个对象。
    • 本质:编译器自动为接口方法生成转发调用。
    • 优点:代码复用、解耦、避免继承限制。
  3. 属性委托 (by + 委托对象实例):

    • 目的:将属性的 get/set 逻辑封装到可复用的委托对象中。
    • 核心约定:委托对象必须实现 getValue (对于 val/var) 和 setValue (对于 var) 操作符函数。
    • 标准库好帮手:lazy (延迟初始化), observable (监听变化), vetoable (拦截赋值), map (映射存储) 等。
    • 自定义委托:遵循 getValue/setValue 约定实现自己的逻辑。
    • 原理:编译器生成访问器方法,这些方法内部调用委托对象的 getValue/setValue
  4. Android 中的高频应用:

    • by viewModels(): 委托给 ViewModelProvider 来获取或创建 ViewModel 实例 (本质是属性委托)。
    • by lazy: 延迟初始化 View (避免 findViewByIdonCreate 中过早执行)、其他开销大的资源。
    • 属性委托到 SharedPreferences/DataStore: 封装存储逻辑。
    • 类委托: 复用实现,比如将 RecyclerView.Adapter 的部分职责委托给 ListAdapterDiffUtil 相关的辅助类。

示例 :SharedPreferences 属性委托

kotlin 复制代码
import android.content.SharedPreferences
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

/**
 * SharedPreferences 属性委托
 * 
 * @param prefs SharedPreferences 实例
 * @param key 存储键名(可选,默认使用属性名)
 * @param defaultValue 默认值(必须提供)
 * @param commit 是否使用 commit 同步提交(默认使用 apply 异步)
 */
class SafePrefsDelegate<T : Any>(
    private val prefs: SharedPreferences,
    private val key: String? = null,
    private val defaultValue: T,
    private val commit: Boolean = false
) : ReadWriteProperty<Any, T> {

    init {
        // 验证支持的类型
        val supportedTypes = listOf(
            String::class, 
            Int::class, 
            Boolean::class, 
            Long::class, 
            Float::class,
            Set::class
        )
        
        require(defaultValue::class in supportedTypes) {
            "Unsupported type: ${defaultValue::class.java.simpleName}. " +
            "Supported types: String, Int, Boolean, Long, Float, Set<String>"
        }
        
        // 验证 Set 类型必须是 Set<String>
        if (defaultValue is Set<*>) {
            require(defaultValue.all { it is String }) {
                "Set must contain only String values"
            }
        }
    }

    override fun getValue(thisRef: Any, property: KProperty<*>): T {
        val usedKey = key ?: property.name
        return when (defaultValue) {
            is String -> prefs.getString(usedKey, defaultValue) as T
            is Int -> prefs.getInt(usedKey, defaultValue) as T
            is Boolean -> prefs.getBoolean(usedKey, defaultValue) as T
            is Long -> prefs.getLong(usedKey, defaultValue) as T
            is Float -> prefs.getFloat(usedKey, defaultValue) as T
            is Set<*> -> prefs.getStringSet(usedKey, defaultValue as Set<String>) as T
            else -> throw IllegalStateException("Unhandled type")
        }
    }

    override fun setValue(thisRef: Any, property: KProperty<*>, value: T) {
        val usedKey = key ?: property.name
        val editor = prefs.edit()
        
        when (value) {
            is String -> editor.putString(usedKey, value)
            is Int -> editor.putInt(usedKey, value)
            is Boolean -> editor.putBoolean(usedKey, value)
            is Long -> editor.putLong(usedKey, value)
            is Float -> editor.putFloat(usedKey, value)
            is Set<*> -> {
                require(value.all { it is String }) { "Set must contain only String values" }
                @Suppress("UNCHECKED_CAST")
                editor.putStringSet(usedKey, value as Set<String>)
            }
            else -> throw IllegalStateException("Unhandled type")
        }
        
        if (commit) {
            editor.commit()
        } else {
            editor.apply()
        }
    }
}

// 扩展函数简化使用
fun <T : Any> SharedPreferences.safePrefs(
    key: String? = null,
    defaultValue: T,
    commit: Boolean = false
): SafePrefsDelegate<T> {
    return SafePrefsDelegate(this, key, defaultValue, commit)
}

// 使用示例
class AppSettings(private val prefs: SharedPreferences) {
    // 使用属性名作为键名
    var userId: String by prefs.safePrefs(defaultValue = "")
    
    // 显式指定键名
    var darkModeEnabled: Boolean by prefs.safePrefs(
        key = "dark_mode",
        defaultValue = false
    )
    
    // 数值类型
    var loginCount: Int by prefs.safePrefs(defaultValue = 0)
    
    // 浮点数
    var volumeLevel: Float by prefs.safePrefs(defaultValue = 0.8f)
    
    // 长整型(时间戳)
    var lastLogin: Long by prefs.safePrefs(defaultValue = 0L)
    
    // 字符串集合
    var favoriteCategories: Set<String> by prefs.safePrefs(
        defaultValue = setOf("news", "sports")
    )
    
    // 同步提交的配置项
    var criticalConfig: String by prefs.safePrefs(
        defaultValue = "default",
        commit = true  // 使用同步提交确保立即写入
    )
}
相关推荐
前端工作日常1 小时前
我理解的`npm pack` 和 `npm install <local-path>`
前端
李剑一2 小时前
说个多年老前端都不知道的标签正确玩法——q标签
前端
嘉小华2 小时前
大白话讲解 Android屏幕适配相关概念(dp、px 和 dpi)
前端
姑苏洛言2 小时前
在开发跑腿小程序集成地图时,遇到的坑,MapContext.includePoints(Object object)接口无效在组件中使用无效?
前端
奇舞精选2 小时前
Prompt 工程实用技巧:掌握高效 AI 交互核心
前端·openai
Danny_FD2 小时前
React中可有可无的优化-对象类型的使用
前端·javascript
用户757582318552 小时前
混合应用开发:企业降本增效之道——面向2025年移动应用开发趋势的实践路径
前端
P1erce2 小时前
记一次微信小程序分包经历
前端
LeeAt2 小时前
从Promise到async/await的逻辑演进
前端·javascript
等一个晴天丶2 小时前
不一样的 TypeScript 入门手册
前端