注册

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

前言


在使用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("111@qq.com")

//对应于被委托属性的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 = "222@qq.com"
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写法还是很值的学习。


作者:元浩875
链接:https://juejin.cn/post/7057675598671380493
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册