「Kotlin 101」委托(Delegation)

背景

今天认识下 Kotlin 中的委托。

委托模式(Delegation pattern)

委托模式是指,两个对象参与处理同一个请求,接收请求的对象,将请求委托给另一个对象处理。

特点:

  • 非继承。
  • 便于基于现有类实现新的类,不用写重复的逻辑。

举例

Kotlin 代码例子:

interface IService {
	fun hello()
	fun print()
}

class ServiceImpl(val cookie: String): IService {
	override fun hello() {
		println("hello, I'm service")
	}

	override fun print() {
		println("cookie bind to this service: $cookie")
	}
}

class Derived(private val impl: IService): IService {
	override fun hello() {
		// 手动实现委托模式
		impl.hello()
	}

	override fun print() {
		// 手动实现委托模式
		impl.print()
	}
}

Kotlin 中的委托模式

Kotlin 很好的支持了委托模式,通关关键字 by 实现。可以划分为两种模式:类委托属性委托

类委托

以前面的例子为例,用 by 关键字实现类委托:

class Derived(private val impl: IService): IService by impl {
}

这样做,类 Derived 中,继承自接口 IService 的方法将会由对象 impl 的同名方法实现,而无需手动重载函数。效果等同于原来,同时大大简化了代码的复杂度。

如果类 Derived 中实现的方法,功能跟 impl 中的有所不同,可以通过重载修改。

class Derived(private val impl: IService): IService by impl {
	override fun print() {
		println("Derived::print")
	}
}

值得注意,接口中的成员变量也能委托!但有一个特殊之处:委托的方法无法访问类中重载后的变量。

interface IService {
	val name: String
	fun print()
}

class ServiceBase(name_: String): IService {
	override val name: String = name_
	override fun print() {
		println("ServiceBase($name)")
	}
}

class Derived(base: IService): IService by base {
	override val name: String = "Derived: ${base.name}"
}

fun main() {
	val base = ServiceBase("base")
	val derived = Derived(base)
	derived.print() // 输出 ServiceBase(base)
	println(derived.name) // 输出 Derived: base
}

属性委托

自定义委托

对属性主要有两个操作:读(get)和写(set)。

Kotlin 提供了可重载运算符 getValue()setValue() 用于读写。

因此如果是要实现属性委托,只需要实现一个类,它里面重载了读写运算符。

class Demo {
	private val str: String by Delegate()
}

class Delegate {
	// 读
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, thank you for delegating '${property.name}' to me!"
    }
    
    // 写
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$value has been assigned to '${property.name}' in $thisRef")
    }
}

属性被委托后,对属性的访问,将会委托到 Delegate()getValue()setValue() 上。

Kotlin 标准库中提供了 3 种接口用于自定义委托。

强烈建议使用标准库提供的接口实现自定义委托,能有效避免重载时把函数签名写错了!

  • ReadOnlyProperty
  • ReadWriteProperty
  • ObservableProperty

除了自定义委托以外,Kotlin 标准库中内置了一些委托类。

截止自 2022 年 4 月 5 日,Kotlin 标准库中提供了 4 种属性委托供使用。

lazy

懒加载委托属性,应该是最常用的一个。
lazy 本质上是一个方法,有两种,都需要接收一个函数块作为入参,用于委托属性的初始化工作。

public actual fun <T> lazy(initializer: () -> T): Lazy<T>

public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T>

用例:

class Demo {
	private val mCount: Int by lazy {
		1
	}
}

特点:

  • 仅当第一次访问时初始化,因而可以用于懒加载。
  • 仅能用于 val 属性。
  • 默认是线程安全的,但可以通过参数 mode 改变这一行为。

Delegates.observable

observable 如它名字,提供了一种监测属性值变换的能力。

public inline fun <T> observable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Unit):
	ReadWriteProperty<Any?, T>

用例:

class Demo {
	private var mCount: Int by Delegates.observable { property, oldValue, newValue ->
		println("property ${property.name} change from $oldValue to $newValue")
	}
}

特点:

  • 提供了属性值变化的监测能力

Delegates.vetoable

vetoableobservable 有些类似,可以监控属性值变化。此外,它还可用来限制属性值是否要改变。

public inline fun <T> vetoable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Boolean):
	ReadWriteProperty<Any?, T>

onChange 将会在写(setValue)属性前调用,并且:

  • 当它返回 false 时,对该属性的写将会失败,属性值保留原来的不变
  • 当它返回 true 时,对该属性的写才生效。

用例:

class Demo {
	private mCount: Int by Delegates.vetoable(0) { property, oldValue, newValue ->
		if (newValue > 0) {	// 仅当设置的值为正数是才允许设置
			true
		} else {
			false
		}
	}
	mCount = 1 // OK, mCount = 1
	mCount += 1 // OK, mCount = 2
	mCount -= 3 // Fail, mCount = 2
}

特点:

  • 能控制属性的写入

Delegates.notNull

如其名,notNull 可以保证委托的属性不为 null

public fun <T : Any> notNull(): ReadWriteProperty<Any?, T>

用例:

class Demo {
	private val mCount: Int? by Delegates.notNull<Int>()
	private var mTotal: Int by Delegates.notNull<Int>()
}

它的用法跟 lateinit var 似乎有些类似,两者的区别在于:

  • lateinit var 不能用于原始类型(primitive type),如 IntLongnotNull 可以。

特点:

  • var 变量类型不能为 null
  • val 变量可以为 null
  • 当未给委托属性赋值时,访问属性会抛出异常 IllegalStateException

委托给其他属性

一个属性的访问还可以委托另一个属性完成,通过 ::

通常,这种做法的场景是:给一个属性起别名。例如需要废弃某个 API 的属性,就可以这么做。

三类属性可以接受委托:

  • 顶层属性
  • 同一类中的成员属性或者扩展属性
  • 其他类实例的成员属性或者扩展属性

示例:

var topLevelInt: Int = 0
class OtherClassDelegate(val otherClassInt: Int = 0)

class MyClass(var memberInt: Int = 0, val otherClass: OtherClassDelegate) {
	var delegatedToTopLevel: Int by ::topLevelInt
	var delegatedToMember: Int by this::memberInt
	val otherClassInt: Int by otherClass::otherClassInt
}

var MyClass.extDelegated: Int by ::topLevelInt

更多技巧

自定义委托提供工厂

还有一种用法, 可以自定义委托构建工厂,用于提供委托类实例。

通常用于实现额外的复杂委托逻辑。

示例:

class DelegateFactory {
    operator fun provideDelegate(thisRef: Any, property: KProperty<*>):
    	// 返回类型可以自定义
    	ReadWriteProperty<Any, String> {
    }
}

顶层函数

lazy,用顶层函数隐藏委托类的创造逻辑。

最佳实践

委托属性有一个技巧:通过将类的属性名和它的值放入一个 map,然后类的属性在定义时,分别委托给这个 map,能实现类的参数化赋值。

示例:

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

val user = User(mapOf(
		"name" to "Kotlin",
		"age" to 25
	)
)

参考资料

  1. 「Kotlin 官网」Delegation
  2. 「Kotlin 官网」Delegated properties
  3. 「Wikipedia」Delegation pattern
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值