Decoding the Future, Line by Line
Kotlin入门-委托(delegation)
Kotlin入门-委托(delegation)

Kotlin入门-委托(delegation)

Kotlin入门-委托(delegation)

委托定义

委托(Delegation) 是一种通过将职责交给另一个对象来实现代码复用的设计模式。它的本质就是对象的组合,将委托方对象传递给被委托方,让被委托方具备委托方的能力。

kotlin 委托通过 by 关键字实现,是 Kotlin 语言层面的特色功能。kotlin 支持2种委托方式:

  1. 类委托:实现继承的替换方案
  2. 属性委托:封装属性访问逻辑
// 类委托
class Derived(b: Base) : Base by b

// 属性委托
val name by lazy { "pp" }

委托原理

类委托

编译器会为委托类生成所有接口方法的转发调用,避免手动实现样板代码

可以看看下面的例子:

//定义接口。
private interface Base {
    fun printMessage()
    fun printMessageLine()
}

//接口的实现类。负责打印Int属性成员变量
private class BaseImpl(private val x: Int) : Base {
    override fun printMessage() { print(x) }
    override fun printMessageLine() { println(x) }
}

//委托类。具备Base接口的能力
private class Derived(b: Base) : Base by b {
    override fun printMessage() { print("abc") }
}

Base:定义接口,包含2个打印方法 BaseImpl:接口实现类。打印 Int 值 Derived:委托对象,虽然没有直接实现 Base 接口,因为委托也具备了Base 接口能力

语法解读:class Derived(b: Base) : Base by b 我们可以把这个代码拆成2部分解读:

  1. class Derived(b: Base) : Base 表示Derived 实现了 Base 接口
  2. by b 表示 Derived 不会显式实现具体的接口方法(当然也可以按需进行overrid实现重写对应的方法),具体的接口实现方法就使用 b 实例进行调用。

我们可以看看反编译后的 Derived 代码:

final class Derived implements Base {
   // $FF: synthetic field
   private final Base $$delegate_0;

   public Derived(@NotNull Base b) {
      Intrinsics.checkNotNullParameter(b, "b");
      super();
      this.$$delegate_0 = b;
   }

   public void printMessageLine() {
      this.$$delegate_0.printMessageLine();
   }

   public void printMessage() {
      String var1 = "abc";
      System.out.print(var1);
   }
}

可以看到:

  1. Derived 确实实现了Base 接口
  2. Derived 确实包含了Base 接口的方法实现,由于没有重写 printMessageLine 方法,所以该方法的实现是由 b 实例对应的printMessageLine 实现的。
  3. Derived 重写了 printMessage,打印字符串 “abc”, 所以该方法内部就没有使用 b 实例的引用了。

由此可以看出,通过 by 关键字可以节省很多模版代码(反编译后的接口方法都需要我们手动编写),这也是kotlin 的优势所在。

应用场景:实现装饰器模式。需要扩展功能,但不直接继承 Android 的 public ContextThemeWrapper(Context base, @StyleRes int themeResId) 就应用了委托的设计机制:在不改变context 功能的基础上,新扩展了指定具体主题的能力,一般可以应用于WindowManager 添加自定义View场景。

属性委托

属性委托通过 getValue() 和 setValue() 运算符重载实现:

class Delegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "Value from Delegate"
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("Value set to $value")
    }
}

应用场景

  1. layzy 懒加载
  2. 观察属性变化(Delegates.observable)
  3. 非空校验(Delegates.vetoable)
  4. SharedPreferences 封装
  5. ViewModel 属性委托(Android Jetpack)

layzy 懒加载

下面是layzy 关键字实现代码,从源码可以看出它确实是通过重写属性的get 方法来实现懒加载的。

LayzyJVM.kt 源码

override val value: T
        get() {
            val _v1 = _value
            if (_v1 !== UNINITIALIZED_VALUE) {
                @Suppress("UNCHECKED_CAST")
                return _v1 as T
            }

            return synchronized(lock) {
                val _v2 = _value
                if (_v2 !== UNINITIALIZED_VALUE) {
                    @Suppress("UNCHECKED_CAST") (_v2 as T)
                } else {
                    val typedValue = initializer!!()
                    _value = typedValue
                    initializer = null
                    typedValue
                }
            }
        }

注意:!== 是比较两个对象是否相等,即是否指向同一块内存地址(并非内容的比较)。

下面是一个懒加载的例子:定义一个name 属性,延迟加载,并在使用时调用2次,观察初始化日志(预期只执行一次),实际上”this is first call name“

class Delegate {
    val name by lazy {
        println("this is first call name")
        "pp"
    }

    init {
        val base = BaseImpl(10)
        Derived(base).printMessage()
        Derived(base).printMessageLine()
        println("first call:$name")
        println("second call:$name")
    }
}

执行测试用例代码会输出下面内容:

abc10
this is first call name
first call:pp
second call:pp

观察属性变化(Delegates.observable)

class Delegate {
    val name by lazy {
        println("this is first call name")
        "pp"
    }
    var title: String by Delegates.observable("初始化pp") { prop, old, new ->
        println("oldValue:$old, newValue:$new")
    }

    init {
        val base = BaseImpl(10)
        Derived(base).printMessage()
        Derived(base).printMessageLine()
        println("first call:$name")
        println("second call:$name")
        title = "new title pp"
    }
}

执行测试用例代码会输出下面内容:

abc10
this is first call name
first call:pp
second call:pp
oldValue:初始化pp, newValue:new title pp

非空校验(Delegates.vetoable)

Delegates.vetoable 和 Delegates.observable 的区别在于调用顺序的差异。

  • Delegates.observable: 在变量赋值之后回调
  • Delegates.vetoable:在变量赋值之前回调,并且可以新增拦截逻辑,拦截方法返回true表示可以赋值,返回false则不能赋值

可以继续看下面 Delegates.vetoable 的例子 这次我们新增了一个成员变量 notNullCheck 字符串,并在初始化时分别赋值 “”,null,”final not null”. 由于添加

class Delegate {
    val name by lazy {
        println("this is first call name")
        "pp"
    }
    var title: String by Delegates.observable("初始化pp") { prop, old, new ->
        println("oldValue:$old, newValue:$new")
    }

    var notNullCheck: String? by Delegates.vetoable("参数不为空") {
            prop, old, new ->

        if (new.isNullOrEmpty()) {
            println("new value is null, so not set new value")
            return@vetoable false
        } else {
            println("new value is not null,so set new value. oldValue:$old, newValue:$new")
            return@vetoable true
        }
    }

    init {
        val base = BaseImpl(10)
        Derived(base).printMessage()
        Derived(base).printMessageLine()
        println("first call:$name")
        println("second call:$name")
        title = "new title pp"
        notNullCheck = ""
        println("notNullCheck first call:$notNullCheck")
        notNullCheck = null
        println("notNullCheck second call:$notNullCheck")
        notNullCheck = "final not null"
        println("notNullCheck third call:$notNullCheck")

    }
}

再执行用例,发现会打印下面日志,基本符合预期:

abc10
this is first call name
first call:pp
second call:pp
oldValue:初始化pp, newValue:new title pp
notNullCheck first call:参数不为空
new value is null, so not set new value
notNullCheck second call:参数不为空
new value is not null,so set new value. oldValue:参数不为空, newValue:final not null
notNullCheck third call:final not null

SharedPreferences 封装

下面是一个 SharedPreferences 委托,即变量的获取和设置 都是通过sp操作的,但是这个操作对于业务来说是透明的,已经在变量定义的时候通过 委托 指定了

class PreferenceDelegate<T>(
    private val prefs: SharedPreferences,
    private val key: String,
    private val defaultValue: T
) {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
        return when (defaultValue) {
            is String -> prefs.getString(key, defaultValue) as T
            is Int -> prefs.getInt(key, defaultValue) as T
            is Boolean -> prefs.getBoolean(key, defaultValue) as T
            else -> throw IllegalArgumentException("Unsupported type")
        }
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        with(prefs.edit()) {
            when (value) {
                is String -> putString(key, value)
                is Int -> putInt(key, value)
                is Boolean -> putBoolean(key, value)
                else -> throw IllegalArgumentException("Unsupported type")
            }.apply()
        }
    }
}

// 使用
var userId by PreferenceDelegate(prefs, "user_id", 0)

由于测试用例中不太好mock sp操作,这里大家感兴趣可以在Android项目中自行验证。

委托 VS 继承

维度 委托 继承
耦合度 低(对象组合) 高(类继承)
灵活性 可运行时切换受托对象 编译时固定
多态支持 需手动转发方法 自动支持
适用场景 需要复用实现但不想耦合父类 明确的 “is-a” 关系

总结

Kotlin 委托的核心价值:

  1. 减少样板代码:自动生成委托方法(类委托)
  2. 关注点分离:将属性管理逻辑抽离(属性委托)
  3. 灵活扩展:通过组合而非继承增强功能

选择时机:

  • 当需要 复用实现但避免继承 时 → 类委托
  • 当需要 统一管理属性行为 时 → 属性委托

通过委托,Kotlin 以简洁的语法实现了强大的设计模式,是提升代码质量的利器。

参考文档

kotlin关键字和操作

类委托

属性委托

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注