Plain's Blog

休想打败我的生活🔥

Kotlin 中委托属性和延迟初始化的工作方式

Plain's Avatar 2020-08-19 Kotlin笔记

  1. 1. 可空类型
  2. 2. 可空类型在自己生命周期内初始化
  3. 3. 非空类型
  4. 4. lateinit: 我稍后会初始化非空类型的属性
  5. 5. 只读属性
  6. 6. 非空只读属性的问题
  7. 7. 懒加载(lazy Initialization)
  8. 8. 属性委托(代理)
  9. 9. by lazy 的工作原理
  10. 10. 带有 lazy() 的委托属性
  11. 11. Lazy 的具体实现
    1. 11.1. SYNCHRONIZED → SynchronizedLazyImpl
    2. 11.2. PUBLICATION → SafePublicationLazyImpl
    3. 11.3. NONE → UnsafeLazyImpl
  12. 12. Lazy 实现的默认行为
    1. 12.1. SynchronizedLazyImpl
  13. 13. Kotlin 的委托属性将会让你快乐 😀

在支持面向对象范式的编程语言中,相信大家对访问属性应该非常熟悉了吧。Kotlin 就提供了很多这样的方法,通过 by lazy 实现属性的懒加载就是一个很好的例子。

在这篇文章中,我们将一起去看看如何使用 Kotlin 中的委托属性以及 by lazy懒加载然后深入了解它们内部的工作原理,一步步揭开它们语法糖衣

可空类型

我认为你们中很多人对 nullable 已经了然于胸,但是让我们再来看看它。我们使用 Kotlin 来开发 Android 时你可能会像如下这样写:

1
2
3
class MainActivity : AppCompatActivity() {
private var helloMessage : String = "Hello"
}

可空类型在自己生命周期内初始化

在上述例子中,在对象创建的时候就初始化,这也没什么大的问题。然而,如果在特定的初始化过程之后引用它,则不能提前声明和使用值,因为它有自己的生命周期来初始化自身。

让我们一起来看下一些熟悉Java代码

1
2
3
4
5
6
7
8
9
public class MainActivity extends AppCompatActivity {
private TextView mWelcomeTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mWelcomeTextView = (TextView) findViewById(R.id.msgView);
}
}

你可以使用 Kotlin 来实现上述代码,通过将上述 mWelcomeTextView 声明成可空类型就可以了。

1
2
3
4
5
6
7
8
9
class MainActivity : AppCompatActivity() {
// 声明成可空类型
private var mWelcomeTextView: TextView? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mWelcomeTextView = findViewById(R.id.msgView) as TextView
}
}

非空类型

上面那个例子代码运行良好,但是在代码中使用属性的之前每次都需要检查它是否为 null 就显得难受了。这一点你完全可以使用非空类型实现它。

1
2
3
4
class MainActivity: AppCompatActivity () { 
private var mWelcomeTextView: TextView
...
}

当然上述代码,你需要使用 lateinit 来告诉编译器,你将稍后为组件 mWelcomeTextView 初始化值。

lateinit: 我稍后会初始化非空类型的属性

与我们通常讨论的延迟初始化(lazy initialization) 不同的是,lateinit 允许编译器识别非空类型属性的值不存储在构造函数阶段以致于可以正常编译。

1
2
3
4
5
6
7
8
class MainActivity : AppCompatActivity() {
private lateinit var mWelcomeTextView: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mWelcomeTextView = findViewById(R.id.msgView) as TextView
}
}

如果你想要了解更多,请查看这里

只读属性

通常,如果组件的字段不是基本数据类型或者内置类型,则可以发现引用是保留在组件整个生命周期中的。

例如,在 Android 应用程序中,大多数的组件引用是在 Acitivity 生命周期中保持不变的。换句话说,这就意味着你很少需要更改组件的引用。

基于这一点,我们可以很容易想到以下这点:

“如果属性的值通常保留在组件的生命周期中,那么只读类型的属性是否足以保持该值?”

我认为可以的,要做到这一点,乍看一眼只需将 var 改为 val 一点改动就可以了。

非空只读属性的问题

但是,当我们声明只读属性时,我们面临的问题是无法定义执行初始化的位置

1
2
3
4
5
6
7
8
9
10
class MainActivity : AppCompatActivity() {
private val mWelcomeTextView: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Where do I move the initialization code?????
// 我应该把这个初始化的代码移到哪呢?????
// mWelcomeTextView = findViewById(R.id.msgView) as TextView
}
}

现在让我们尝试解决最后一个问题:“我们在哪初始化只读属性呢?”

懒加载(lazy Initialization)

当实现在 Kotlin 中执行延迟初始化的只读属性时,by lazy 也许就特别有用了。

by lazy{...} 执行初始化程序,其中首先使用的是定义的属性,而不是它的声明。

1
2
3
4
5
6
7
8
9
10
11
12
13
class MainActivity : AppCompatActivity() {
private val messageView : TextView by lazy {
// 下面这段代码将在第一次访问 messageView 时执行
findViewById(R.id.message_view) as TextView
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
fun onSayHello() {
// 真正初始化将会在这执行!!
messageView.text = "Hello"
}
}

现在,我们可以声明一个只读属性,而不必担心 messageView 的初始化点的问题。让我们看看懒加载背后原理是怎么样的。

属性委托(代理)

Delegation 的意思就是委托。它意味着通过委托者可以执行某些操作,而不是直接通过原始访问者执行操作。

属性委托委托属性getter/setter 方法,它允许委托对象在读取和写入值时插入执行一些中间操作。

Kotlin 支持将接口(类委托)或访问器(委托属性)的实现委托给另一个对象。

Delegation is a something with a historic background. :) (Source:Wikipedia commons)

你可以通过 by <delegate> 形式来声明一个委托属性

1
val / var <property name>: <Type> by <delegate>

属性的委托可以像如下方式定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Delegate {
operator fun getValue(
thisRef: Any?,
property: KProperty<*>
): String {
// return value
}
operator fun setValue(
thisRef: Any?,
property: KProperty<*>, value: String
) {
// assign
}
}

对值的所有读取操作都会委托调用 getValue() 方法,同理,对值的所有写操作都会委托调用 setValue() 方法。

by lazy 的工作原理

现在让我们再次重新研究下上述例子中属性的代码。

它实际上就是一个属性委托

我们可以把 by lazy 修饰的属性理解为是具有 lazy 委托委托属性

所以,lazy 是如何工作的呢?让我们一起在 Kotlin 标准库参考中总结 lazy() 方法,如下所示:

  1. lazy() 返回的是一个存储在 lambda 初始化器中的 Lazy<T> 类型实例。
  2. getter 的第一次调用执行传递给 lazy()lambda 并存储其结果。
  3. 后面的话,getter 调用只返回存储中的值。

简单地说,lazy创建一个实例,在第一次访问属性值时执行初始化,存储结果并返回存储的值

带有 lazy() 的委托属性

让我们编写一个简单的 Kotlin 代码来检查 lazy 的实现。

1
2
3
class Demo {
val myName: String by lazy { "John" }
}

如果你将其反编译Java 代码,则可以看到以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public final class Demo {
@NotNull
private final Lazy myName$delegate;

// $FF: synthetic field
static final KProperty[] $$delegatedProperties = ...
@NotNull
public final String getMyName() {
Lazy var1 = this.myName$delegate;
KProperty var3 = $$delegatedProperties[0];
return (String)var1.getValue();
}
public Demo() {
this.myName$delegate =
LazyKt.lazy((Function0)null.INSTANCE);
}
}
  • $delegate 后缀被拼接到字段名称后面: myName$delegate
  • 注意 myName$delegate 的类型是 Lazy 类型不是 String 类型
  • 在构造器中,LazyKt.lazy() 函数返回值赋值给了** myName$delegate**
  • LazyKt.lazy() 方法负责执行指定的初始化块

调用 getMyName() 方法实际过程是将通过调用 myName$delegateLazy 实例中的 getValue() 方法并返回相应的值。

Lazy 的具体实现

lazy() 方法返回的是一个 Lazy<T> 类型的对象,该对象处理 lambda 函数(初始化程序块),根据线程执行模式(LazyThreadSafetyMode)以稍微几种不同的方式执行初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
@kotlin.jvm.JvmVersion
public fun <T> lazy(
mode: LazyThreadSafetyMode,
initializer: () -> T
): Lazy<T> =
when (mode) {
LazyThreadSafetyMode.SYNCHRONIZED ->
SynchronizedLazyImpl(initializer)
LazyThreadSafetyMode.PUBLICATION ->
SafePublicationLazyImpl(initializer)
LazyThreadSafetyMode.NONE ->
UnsafeLazyImpl(initializer)
}

所有这些都负责调用给定的 lambda 块进行延迟初始化

SYNCHRONIZED → SynchronizedLazyImpl

  • 初始化操作仅仅在首先调用的第一个线程上执行
  • 然后,其他线程将引用缓存后的值。
  • 默认模式就是(LazyThreadSafetyMode.SYNCHRONIZED)

PUBLICATION → SafePublicationLazyImpl

它可以同时多个线程中调用,并且可以在全部或部分线程同时进行初始化
但是,如果某个值已由另一个线程初始化,则将返回该值不执行初始化

NONE → UnsafeLazyImpl

  • 只需在第一次访问时初始化它,或返回存储的值。
  • 不考虑多线程,所以它不是线程安全的。

Lazy 实现的默认行为

SynchronizedLazyImplSafePublicationLazyImplUnsafeLazyImpl 通过以下过程执行延迟初始化。我们来看看前面的例子。

简单的代码示例始终是一件好事!:)

  1. 将传入的初始化 lambda 块存储在属性的 initializer 中。

初始化 lambda 不会执行,但会存储起来以供以后执行。

  1. 通过属性 _value 来存储值。此属性最开始初始值UNINITIALIZED_VALUE

分配 UNINITIALIZED\_VALUE 表示该值未初始化。

  1. 在执行读取操作(属性 get 访问器)时,如果 _value 的值是最开始初始值 UNINITIALIZED_VALUE,那么就会去执行 initializer 初始化器。

延迟初始化是通过在首次访问时执行初始化程序来执行的。

  1. 在执行读取操作(属性 get 访问器)时,如果 _value 的值不是等于** UNINITIALIZED_VALUE**, 那就说明初始化操作已经执行完成了。

现在存储的值已经存在,对 getValue() 的调用将返回“John”。

SynchronizedLazyImpl

如果你没有明确指定具体模式,延迟具体实现就是 SynchronizedLazyImpl,它默认只执行一次初始化。我们来看看它的实现代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
private object UNINITIALIZED_VALUE
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
// final field is required
// to enable safe publication of constructed instance
private val lock = lock ?: this
override val value: T
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)
}

这看起来有点复杂。但它只是多线程一般实现

  • 使用 synchronized() 同步块执行初始化
  • 由于初始化可能已经由另一个线程完成,它会进行双重锁检测(DCL),如果已经完成了初始化,则返回存储的值
  • 如果它未初始化,它将执行 lambda 表达式并存储返回值。那么随后这个 initializer 将会置为 null,因为初始化完成后就不再需要它了。

Kotlin 的委托属性将会让你快乐 😀

当然,延迟初始化有时会导致问题发生或通过绕过控制流并在异常情况下生成正常值来使调试变得困难。

但是,如果你对这些情况非常谨慎,那么Kotlin的延迟初始化可以使我们更加自由地避免对线程安全性和性能的担忧。

我们还研究了延迟初始化是运算符 bylazy 函数的共同作用的结果。还有更多委托,如 observablenotNull 。如有必要,你还可以实现有趣的委托属性!

本文作者 : [原] Chang W. Doh,[译] 极客熊猫
©️ 转载文章,版权归原作者所有
本文链接 : https://plain-dev.com/kotlin-delegated-properties-and-lazy-initialization-work/

本文最后更新于 天前,文中所描述的信息可能已发生改变