看似普通的Android开发黑科技 - Kotlin 委托

1. 前言

我们都知道委托模式也叫代理模式,指的是一个对象接收到请求之后将请求转交由另外的对象来处理。
Kotlin直接支持委托模式,kotlin通过关键字by实现委托,更加优雅,简洁。

我们在使用Kotlin开发的时候,经常会使用by来实现一些操作,使用by lazy来实现懒加载。
虽然日常用的比较多,但却不明白其原理,lazy又代表着什么 ?
还有看似普通的by,实则可以实现很多的功能,可以更方便我们的开发。
接下来,我们来陆续回答这些问题。

Kotlin中,委托分为类委托属性委托

2. 类委托

类委托的核心思想是将一个类的一些具体实现委托给另一个类去完成。
我们新建一个接口Base,声明方法print()

interface Base {
    
    
    fun print()
}

然后创建一个实现类Impl1

class Impl1() : Base {
    
    
    override fun print1() {
    
    
        Log.i("TAG", "------打印了print1------")
    }

    override fun print2() {
    
    
        Log.i("TAG", "------打印了print2------")
    }
}

接着,创建一个实现类Impl2,其实现会委托给Impl1
同时,重写了print1方法,这意味着print1会由Impl2自己来实现

class Impl2(base: Base) : Base by base {
    
    
    override fun print1() {
    
    
        Log.i("TAG", "------重写了print1------")
    }
}

运行程序,可以发现打印如下内容

TAG: ------重写了print1------
TAG: ------打印了print2------

3. 属性委托

属性委托指的是一个类的某个属性值不是在类中直接进行定义,而是将其委托给一个代理类,从而实现对该类的属性统一管理。

3.1 属性委托的示例

首先,我们需要先定义一个被委托类,实现setValue / getValue方法,这是使用by的一个约定,否则是无法使用by的。
注意 : 如果是只读的(val)只需实现getValue(),如果读写都需要(var),setValue / getValue都需要实现

class Delegate {
    
    
    private var value: String = ""

    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
    
    
        println("$thisRef, 这里委托了 ${
      
      property.name} 属性")
        return value
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
    
    
        println("$thisRef${
      
      property.name} 属性赋值为 $value")
        this.value = value
    }
}

需要注意的是setValue / getValue方法的几个参数

  • thisRef:必须与 属性所有者 类型(对于扩展属性 - 指被扩展的类型)相同或者是它的超类型
  • property:必须是类型 KProperty<*>或其超类型
  • value:必须与属性同类型或者是它的子类型

接着定义包含属性委托的类

class Example {
    
    
    var p: String by Delegate()
}

进行调用

fun main() {
    
    
    val example = Example()
    println("第一次打印:" + example.p) // 访问该属性,调用 getValue() 函数

    example.p = "Heiko" // 调用 setValue() 函数
    println("第二次打印:" + example.p)
}

可以看到打印的日志如下

I/System.out: com.heiko.koltintest.Test2$Example@9c2d655, 这里委托了 p 属性
I/System.out: 第一次打印:
I/System.out: com.heiko.koltintest.Test2$Example@9c2d655 的 p 属性赋值为 Heiko
I/System.out: com.heiko.koltintest.Test2$Example@9c2d655, 这里委托了 p 属性
I/System.out: 第二次打印:Heiko

3.2 将委托属性和被委托类合并到一个类中

class Example {
    
    
    var value: String = ""

    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
    
    
        println("$thisRef, 这里委托了 ${
      
      property.name} 属性")
        return value
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
    
    
        println("$thisRef${
      
      property.name} 属性赋值为 $value")
        this.value = value
    }
}

fun main() {
    
    
    var e by Example()
    println(e) // 访问该属性,调用 getValue() 函数

    e = "Heiko" // 调用 setValue() 函数
    println(e)
}

打印日志如下

I/System.out: null, 这里委托了 e 属性
I/System.out: null 的 e 属性赋值为 Heiko
I/System.out: null, 这里委托了 e 属性
I/System.out: Heiko

3.3 结合inline内联函数一起使用

class Example {
    
    
    var value: String = ""
}

inline operator fun Example.getValue(thisRef: Any?, property: KProperty<*>): String {
    
    
    println("$thisRef, 这里委托了 ${
      
      property.name} 属性")
    return value
}

inline operator fun  Example.setValue(
    thisRef: Any?,
    property: KProperty<*>,
    value: String
) {
    
    
    println("$thisRef${
      
      property.name} 属性赋值为 $value")
    this.value = value
}

fun main() {
    
    
    var example by Example()
    println(example) // 访问该属性,调用 getValue() 函数

    example = "Heiko" // 调用 setValue() 函数
    println(example)
}

打印日志如下

I/System.out: null, 这里委托了 example 属性
I/System.out: null 的 example 属性赋值为 Heiko
I/System.out: null, 这里委托了 example 属性
I/System.out: Heiko

3.4 一种更简单的方式

实现属性委托,需要实现getValue/setValue方法,这相对麻烦,也容易写错,为此,kotlin为我们提供了operator方法的 ReadOnlyProperty/ReadWriteProperty 接口。

interface ReadOnlyProperty<in R, out T> {
    
    
    operator fun getValue(thisRef: R, property: KProperty<*>): T
}

interface ReadWriteProperty<in R, T> {
    
    
    operator fun getValue(thisRef: R, property: KProperty<*>): T
    operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
}
  • ReadOnlyProperty : val属性实现这个
  • ReadWriteProperty : var属性实现这个

我们来修改下代码

class Example : ReadWriteProperty<Any?, String> {
    
    
    var value: String = ""

    override operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
    
    
        println("$thisRef, 这里委托了 ${
      
      property.name} 属性")
        return value
    }

    override operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
    
    
        println("$thisRef${
      
      property.name} 属性赋值为 $value")
        this.value = value
    }
}

fun main() {
    
    
    var e by Example()
    println(e) // 访问该属性,调用 getValue() 函数

    e = "Heiko" // 调用 setValue() 函数
    println(e)
}

可以发现运行也是一样的

4. kotlin标准库中提供的几种委托

  • 延迟属性(lazy): 其值只在首次访问时才进行初始化
  • 可观察属性(observable / vetoable): 监听器会收到有关此属性变更的通知
  • 把多个属性储存在一个映射(map)中,而不是每个存在单独的字段中
  • 委托给另一个属性(by this::xxxx) : 用来以一种向后兼容的方式重命名一个属性的时候
  • notNull : 适用于那些无法在初始化阶段就确定属性值的场合

4.1 延迟属性 Lazy

lazy() 是接受一个 lambda 并返回一个 Lazy <T> 实例的函数,返回的实例可以作为实现延迟属性的委托:第一次调用 get() 会执行已传递给 lazy()lambda 表达式并记录结果, 后续调用 get() 只是返回记录的结果。

val lazyValue: String by lazy {
    
    
    println("computed!")
    "Hello"
}

lazy还可以传入参数 (如果不传入参数,那么使用的是LazyThreadSafetyMode.SYNCHRONIZED)

public enum class LazyThreadSafetyMode {
    
    
    SYNCHRONIZED,
    PUBLICATION,
    NONE,
}
  • LazyThreadSafetyMode.SYNCHRONIZED: 添加同步锁,使lazy延迟初始化线程安全
  • LazyThreadSafetyMode. PUBLICATION:初始化的lambda表达式可以在同一时间被多次调用,但是只有第一个返回的值作为初始化的值。
  • LazyThreadSafetyMode. NONE:没有同步锁,多线程访问时候,初始化的值是未知的,非线程安全,一般情况下,不推荐使用这种方式,除非你能保证初始化和属性始终在同一个线程

4.2 可观察属性 Observable

Delegates.observable() 接受两个参数:初始值与修改时处理程序(handler)。 每当我们给属性赋值时会调用该处理程序(在赋值后执行)。它有三个参数:被赋值的属性、旧值与新值:

import kotlin.properties.Delegates

class User {
    
    
    var name: String by Delegates.observable("<no name>") {
    
    
        prop, old, new ->
        println("$old -> $new")
    }
}

fun main() {
    
    
    val user = User()
    user.name = "first"
    user.name = "second"
}

4.2.1 vetoable 函数

vetoable 与 observable一样,可以观察属性值的变化,不同的是,vetoable可以控制返回值来决定属性是否生效

4.3 将属性储存在映射中

一个常见的用例是在一个映射(map)里存储属性的值。 这经常出现在像解析 JSON 或者做其他“动态”事情的应用中。 在这种情况下,你可以使用映射实例自身作为委托来实现委托属性。

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 25
))

委托属性会从这个映射中取值(通过字符串键——属性的名称)

println(user.name) // Prints "John Doe"
println(user.age)  // Prints 25

这也适用于 var 属性,需要把只读的 Map 换成 MutableMap

class MutableUser(val map: MutableMap<String, Any?>) {
    
    
    var name: String by map
    var age: Int     by map
}

4.4 委托给另一个属性

从 Kotlin 1.4 开始,一个属性可以把它的 getter 与 setter 委托给另一个属性。这种委托对于顶层和类的属性(成员和扩展)都可用。该委托属性可以为

  • 顶层属性
  • 同一个类的成员或扩展属性
  • 另一个类的成员或扩展属性

为将一个属性委托给另一个属性,应在委托名称中使用恰当的 :: 限定符,例如,this::delegate 或 MyClass::delegate。

class MyClass(var memberInt: Int, val anotherClassInstance: ClassWithDelegate) {
    
    
    var delegatedToMember: Int by this::memberInt
    var delegatedToTopLevel: Int by ::topLevelInt
    
    val delegatedToAnotherClass: Int by anotherClassInstance::anotherClassInt
}
var MyClass.extDelegated: Int by ::topLevelInt

这是很有用的,例如,当想要以一种向后兼容的方式重命名一个属性的时候:引入一个新的属性、 使用 @Deprecated 注解来注解旧的属性、并委托其实现。

class MyClass {
    
    
   var newName: Int = 0
   @Deprecated("Use 'newName' instead", ReplaceWith("newName"))
   var oldName: Int by this::newName
}

fun main() {
    
    
   val myClass = MyClass()
   // 通知:'oldName: Int' is deprecated.
   // Use 'newName' instead
   myClass.oldName = 42
   println(myClass.newName) // 42
}

4.5 notNull

notNull适用于那些无法在初始化阶段就确定属性值的场合

class Foo{
    
    
     var notNullBar:String by Delegates.notNull<String>()
}

foo.notNullBar="bar"
println(foo.notNullBar)

需要注意,如果属性在赋值前就被访问的话则会抛出异常

5. ViewBinding和kotlin委托结合使用

除了以上的功能,还有很多地方可以用到kotlin委托,比如我们可以用来封装SharedPreferences,View属性获取/设置的委托,当然,还能和ViewBinding结合使用,使ViewBinding的声明更加便捷。

以前我们使用ViewBinding,需要如下定义

class MainActivity : AppCompatActivity(R.layout.activity_main) {
    
    
    private val binding : ActivityMainBinding by viewBinding(ActivityMainBinding::bind)

    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)
    }
}

我们可以使用Kotlin委托去实现ViewBindinginflate过程

class ActivityViewBindings<in A : ComponentActivity, out T : ViewBinding>(private val viewBinder: (A) -> T) :
    ReadOnlyProperty<A, T> {
    
    
    override fun getValue(thisRef: A, property: KProperty<*>): T {
    
    
        return viewBinder(thisRef)
    }
}

public inline fun <A : ComponentActivity, T : ViewBinding> ComponentActivity.viewBinding(
    crossinline vbFactory: (View) -> T,
    crossinline viewProvider: (A) -> View = ::findRootView
): ActivityViewBindings<A, T> {
    
    
    return ActivityViewBindings {
    
     activity -> vbFactory(viewProvider(activity)) }
}

fun findRootView(activity: Activity): View {
    
    
    val contentView = activity.findViewById<ViewGroup>(android.R.id.content)
    checkNotNull(contentView) {
    
     "Activity has no content view" }
    return when (contentView.childCount) {
    
    
        1 -> contentView.getChildAt(0)
        0 -> error("Content view has no children. Provide a root view explicitly")
        else -> error("More than one child view found in the Activity content view")
    }
}

然后进行使用,就可以省略ActivityMainBinding.inflate()这一步了

class MainActivity : AppCompatActivity(R.layout.activity_main) {
    
    
    private val binding : ActivityMainBinding by viewBinding(ActivityMainBinding::bind)

    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)
    }
}

当然,这只是最粗略的写法,还有其他更简单的写法,具体详见我的另一篇博客 ViewBinding与Kotlin委托结合使用,去除setContentView,其实现原理解析

6. 实现一个自己的by lazy

我们来写个伪代码,来实现一个自己的by lazy,实现的效果如下

private val hello : String by myLazy {
    
    
    "hello"
}

6.1 定义MyLazy接口

首先,我们定义一个接口叫做MyLazy

public interface MyLazy<out T> {
    
    
    public val value: T
}

6.2 实现Kotlin委托

然后,按照Kotlin委托的规则,我们需要实现getValue()

public inline operator fun <T> MyLazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value

6.3 实现MyLazy接口实现类

然后,定义MyLazy接口的实现类SynchronizedMyLazyImpl

internal object UNINITIALIZED_VALUE

private class SynchronizedMyLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : MyLazy<T> {
    
    
    private var initializer: (() -> T)? = initializer
    //用来保存值,如果已经被初始化,就不是默认值了
    @Volatile private var _value: Any? = UNINITIALIZED_VALUE
    private val lock = lock ?: this

    override val value: T
        get() {
    
     //其实就是一个双重校验锁实现的单例
            val _v1 = _value
            if (_v1 !== UNINITIALIZED_VALUE) {
    
    
                @Suppress("UNCHECKED_CAST")
                //当value已初始化,直接返回其值
                return _v1 as T
            }

            return synchronized(lock) {
    
    
                val _v2 = _value
                //通过加锁的方式,再次进行判断value是否已初始化
                if (_v2 !== UNINITIALIZED_VALUE) {
    
    
                    //当value已初始化,直接返回其值
                    @Suppress("UNCHECKED_CAST") (_v2 as T)
                } else {
    
    
                    //当value未初始化,调用 initializer!!() 进行初始化
                    val typedValue = initializer!!()
                    //赋值 _value
                    _value = typedValue
                    initializer = null
                    typedValue
                }
            }
        }
}

6.4 定义函数myLazy

定义函数myLazy,参数需要传入initializer: () -> T

public fun <T> myLazy(initializer: () -> T): MyLazy<T> = SynchronizedMyLazyImpl(initializer)

6.5 进行使用

然后进行使用就可以了

private val hello : String by myLazy {
    
    
    "hello"
}

推荐我的另一篇文章 :
ViewBinding与Kotlin委托结合使用,去除setContentView,其实现原理解析

参考
一文彻底搞懂Kotlin中的委托
委托 - Kotlin 语言中文站
Kotlin by 关键字
Kotlin常用的by lazy你真的了解吗
Kotlin原理-by关键字
kotlin by 关键字用法及使用场景

猜你喜欢

转载自blog.csdn.net/EthanCo/article/details/126707002