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

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

前言

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

正文

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

委托属性

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

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

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

Java的属性

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

//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的类来说,属性定义就非常简单了,比如下面类:

class People(){
    val name: String? = null
    var age: Int? = null
}
复制代码

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

val people = People()
//调用name属性的getter方法
people.name
//调用age属性的setter方法
people.age = 12
复制代码

这时就有了疑问,为什么上面代码定义name时我在后面给他赋值了即null值,和Java一样不赋值可以吗 还有个疑问就是在Java中是把属性的值保存在字段中,那Kotlin呢,比如name这个属性的值就保存给它自己吗

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

Kotlin属性访问器

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

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的支持字段来保存属性。如下代码:

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方法来获取,会在多个类都要写一遍,十分不符合代码设计,所以委托属性至关重要。

委托属性的实现

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

class People{
    val name: String? = null
    var age: Int = 0
    val isAbove18: Boolean = false
    //email属性进行委托,把它委托给ProduceEmail类
    var email: String by ProduceEmail()
}
复制代码

假如People的email属性需要委托,上面代码编译器会编译成如下:

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个函数:

//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)
    }

}
复制代码

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

class People{
    val name: String? = null
    var age: Int = 0
    val isAbove18: Boolean = false
    //委托属性
    var email: String by ProduceEmail()
}
复制代码

然后看一下调用地方:

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是个啥,直接看代码:

//这里使用by lazy惰性初始化一个实例
val instance: DataStoreManager by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
    DataStoreManager(store) }
复制代码

比如上面代码,使用惰性初始化初始了一个实例,我们来看一下这个by的实现:

//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便是返回的值,我们来看一下:

//惰性初始化类
public interface Lazy<out T> {
    
    //懒加载的值,一旦被赋值,将不会被改变
    public val value: T

    //表示是否已经初始化
    public fun isInitialized(): Boolean
}
复制代码

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

//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的模式代码:

//线程安全模式下的单例
private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, 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写法还是很值的学习。

Guess you like

Origin juejin.im/post/7057675598671380493