Kotlin常用的by lazy你真的了解吗

语言: CN / TW / HK

highlight: a11y-dark

「这是我参与2022首次更文挑战的第10天,活动详情查看:2022首次更文挑战

前言

在使用Kotlin语言进行开发时,我相信很多开发者都信手拈来地使用by或者by lazy来简化你的属性初始化,但是by lazy涉及的知识点真的了解吗 假如让你实现这个功能,你会如何设计。

正文

话不多说,我们从简单的属性委托by来说起。

委托属性

什么是委托属性呢,比较官方的说法就是假如你想实现一个比较复杂的属性,它们处理起来比把值保存在支持字段中更复杂,但是却不想在每个访问器都重复这样的逻辑,于是把获取这个属性实例的工作交给了一个辅助对象,这个辅助对象就是委托。

比如可以把这个属性的值保存在数据库中,一个Map中等,而不是直接调用其访问器。

看完这个委托属性的定义,假如你不熟悉Kotlin也可以理解,就是我这个类的实例由另一个辅助类对象来提供,但是这时你可能会疑惑,上面定义中说的支持字段和访问器是什么呢,这里顺便给不熟悉Kotlin的同学普及一波。

Java的属性

当你定义一个Java类时,在定义字段时并不是所有字段都是属性,比如下面代码:

```js //Java类 public class Phone {

//3个字段
private String name;
private int price;
private int color;

//name字段访问器
private String getName() {
    return name;
}

private void setName(String name){
    this.name = name;
}

//price字段访问器
private int getPrice() {
    return price;
}

private void setPrice(int price){
    this.price = price;
}

} ``` 上面我在Phone类中定义了3个字段,但是只有name和price是Phone的属性,因为这2个字段有对应的get和set,也只有符合有getter和setter的字段才叫做属性。

这也能看出Java类的属性值是保存在字段中的,当然你也可以定义setXX函数和getXX函数,既然XX属性没有地方保存,XX也是类的属性。

Kotlin的属性

而对于Kotlin的类来说,属性定义就非常简单了,比如下面类:

js class People(){ val name: String? = null var age: Int? = null } 在Kotlin的类中只要使用val/var定义的字段,它就是类的属性,然后会自带getter和setter方法(val属性相当于Java的final变量,是没有set方法的),比如下面:

js val people = People() //调用name属性的getter方法 people.name //调用age属性的setter方法 people.age = 12 这时就有了疑问,为什么上面代码定义name时我在后面给他赋值了即null值,和Java一样不赋值可以吗 还有个疑问就是在Java中是把属性的值保存在字段中,那Kotlin呢,比如name这个属性的值就保存给它自己吗

带着问题,我们继续分析。

Kotlin属性访问器

前面我们可知Java中的属性是保存在字段中,或者不要字段,其实Kotlin也可以,这个就是给属性定义自定义setter方法和getter方法,如下代码:

js class People(){ val name: String? = null var age: Int = 0 //定义了isAbove18这个属性 var isAbove18: Boolean = false get() = age > 18 } 比如这里自定义了get访问器,当再访问这个属性时,便会调用其get方法,然后进行返回值。

Kotlin属性支持字段field

这时一想那Kotlin的属性值保存在哪里呢,Kotlin会使用一个field的支持字段来保存属性。如下代码:

```js class People{ val name: String? = null var age: Int = 0 //返回field的值 get() = field //设置field的值 set(value){ Log.i("People", "旧值是$field 新值是$value ") field = value }

var isAbove18: Boolean = false
    get() = age > 18

} ``` 可以发现每个属性都会有个支持字段field来保存属性的值。

好了,为了介绍为什么Kotlin要有委托属性这个机制,假如我在一个类中,需要定义一个属性,这时获取属性的值如果使用get方法来获取,会在多个类都要写一遍,十分不符合代码设计,所以委托属性至关重要。

委托属性的实现

在前面说委托属性的概念时就说了,这个属性的值需要由一个新类来代理处理,这就是委托属性,那我们也可以大概猜出委托属性的底层逻辑,大致如下面代码:

js class People{ val name: String? = null var age: Int = 0 val isAbove18: Boolean = false //email属性进行委托,把它委托给ProduceEmail类 var email: String by ProduceEmail() } 假如People的email属性需要委托,上面代码编译器会编译成如下:

js class People{ val name: String? = null var age: Int = 0 val isAbove18: Boolean = false //委托类的实例 private val productEmail = ProduceEmail() //委托属性 var email: String //访问器从委托类实例获取值 get() = productEmail.getValue() //设置值把值设置进委托类实例 set(value) = productEmail.setValue(value) } 当然上面代码是编译不过的,只是说一下委托的实现大致原理。那假如想使ProduceEmail类真的具有这个功能,需要如何实现呢。

by约定

其实我们经常使用 by 关键字它是一种约定,是对啥的约定呢 是对委托类的方法的约定,关于啥是约定,一句话说明白就是简化函数调用,具体可以查看我之前的文章:

# Kotlin invoke约定,让Kotlin代码更简洁

那这里的by约定简化了啥函数调用呢 其实也就是属性的get方法和set方法,当然委托类需要定义相应的函数,也就是下面这2个函数:

```js //by约定能正常使用的方法 class ProduceEmail(){

private val emails = arrayListOf("[email protected]")

//对应于被委托属性的get函数
operator fun getValue(people: People, property: KProperty<*>): String {
    Log.i("zyh", "getValue: 操作的属性名是 ${property.name}")
    return emails.last()
}

//对于被委托属性的get函数
operator fun setValue(people: People, property: KProperty<*>, s: String) {
    emails.add(s)
}

} ``` 定义完上面委托类,便可以进行委托属性了:

js class People{ val name: String? = null var age: Int = 0 val isAbove18: Boolean = false //委托属性 var email: String by ProduceEmail() } 然后看一下调用地方: js val people = People() Log.i("zyh", "onCreate: ${people.email}") people.email = "[email protected]" Log.i("zyh", "onCreate: ${people.email}") 打印如下:

image.png

会发现每次调用email属性的访问器方法时,都会调用委托类的方法。

关于委托类中的方法,当你使用by关键字时,IDE会自动提醒,提醒如下:

image.png

比如getValue方法中的参数,第一个就是接收者了,你这个要委托的属性是哪个类的,第二个就是属性了,关于KProperty不熟悉的同学可以查看文章:

# Kotlin反射全解析3 -- 大展身手的KProperty

它就代表这属性,可以调用其中的一些方法来获取属性的信息。

而且方法必须使用operator关键字修饰,这是重载操作符的必须步骤,想使用约定,就必须这样干。

by lazy的实现

由前面明白了by的原理,我们接着来看一下我们经常使用的by lazy是个啥,直接看代码:

js //这里使用by lazy惰性初始化一个实例 val instance: DataStoreManager by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { DataStoreManager(store) } 比如上面代码,使用惰性初始化初始了一个实例,我们来看一下这个by的实现:

js //by代码 @kotlin.internal.InlineOnly public inline operator fun <T> Lazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value 哦,会发现它是Lazy类的一个扩展函数,按照前面我们对by的理解,它就是把被委托的属性的get函数和getValue进行配对,所以可以想象在Lazy< T >类中,这个value便是返回的值,我们来看一下:

```js //惰性初始化类 public interface Lazy {

//懒加载的值,一旦被赋值,将不会被改变
public val value: T

//表示是否已经初始化
public fun isInitialized(): Boolean

} ``` 到这里我们注意一下 by lazy的lazy,这个就是一个高阶函数,来创建Lazy实例的,lazy源码:

js //lazy源码 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) } 这里会发现第一个参数便是线程同步的模式,第二个参数是初始化器,我们就直接看一下最常见的SYNCHRONIZED的模式代码: ```js //线程安全模式下的单例 private class SynchronizedLazyImpl(initializer: () -> T, lock: Any? = null) : Lazy, Serializable { private var initializer: (() -> T)? = initializer //用来保存值,当已经被初始化时则不是默认值 @Volatile private var _value: Any? = UNINITIALIZED_VALUE //锁 private val lock = lock ?: this

override val value: T
    //见分析1
    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
            }
        }
    }

//是否已经完成
override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE

override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."

private fun writeReplace(): Any = InitializedLazyImpl(value)

} ``` 分析1:这个单例实现是不是有点眼熟,没错它就是双重校验锁实现的单例,假如你对双重校验锁的实现单例方式还不是很明白可以查看文章:

# Java双重校验锁单例原理 赶快看进来

这里实现懒加载单例的模式就是双重校验锁,2次判空以及volatile关键字都是有作用的,这里不再赘述。

总结

先搞明白by的原理,再理解by lazy就非常好理解了,虽然这些关键字我们经常使用,不过看一下其源码实现还是很舒爽的,尤其是Kotlin的高阶函数的一些SDK写法还是很值的学习。