翻译说明:
原标题: How Kotlin’s delegated properties and lazy-initialization work
原文地址:
medium.com/til-kotlin/…
原文作者:
Chang W. Doh
在支持面向对象范式的编程语言中,相信大家对访问属性应该非常熟悉了吧。Kotlin就提供了很多这样的方法,通过
by lazy
实现属性的
懒加载
就是一个很好的例子。
在这篇文章中,我们将一起去看看如何使用Kotlin中的委托属性以及
by lazy
的懒加载然后深入了解它们内部的工作原理,一步步揭开它们语法糖衣。
可空类型
我认为你们中很多人对
nullable
已经了然于胸,但是让我们再来看看它。我们使用Kotlin来开发Android时你可能会像如下这样写:
class MainActivity : AppCompatActivity() {
private var helloMessage : String = "Hello"
}
复制代码
可空类型在自己生命周期内初始化
在上述例子中,在对象创建的时候就初始化,这也没什么大的问题。然而,如果在特定的初始化过程之后引用它,则不能提前声明和使用值,因为它有自己的生命周期来初始化自身。
让我们一起来看下一些熟悉Java代码
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
声明成可空类型就可以了.
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就显得难受了。这一点你完全可以使用非空类型实现它。
class MainActivity: AppCompatActivity () {
private var mWelcomeTextView: TextView
...
}
复制代码
当然上述代码,你需要使用
lateinit
来告诉编译器,你将稍后为组件
mWelcomeTextView
初始化值。
lateinit: 我稍后会初始化非空类型的属性
与我们通常讨论的
延迟初始化(lazy initialization)
不同的是,
lateinit
允许编译器识别非空类型属性的值不存储在构造函数阶段以致于可以正常编译。
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
一点改动就可以了。
非空只读属性的问题
但是,当我们声明只读属性时,我们面临的问题是无法定义执行初始化的位置
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{...}
执行初始化程序,其中首先使用的是定义的属性,而不是它的声明。
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>
形式来声明一个
委托属性
val / var <property name>: <Type> by <delegate>
复制代码
属性的委托可以像如下方式定义:
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()
的委托属性
lazy()
让我们编写一个简单的Kotlin代码来检查
lazy
的实现。
class Demo {
val myName: String by lazy { "John" }
}
复制代码
如果你将其反编译为Java代码,则可以看到以下代码:
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$delegate
的
Lazy
实例中的
getValue()
方法并返回相应的值。
Lazy的具体实现
lazy()
方法返回的是一个
Lazy<T>
类型的对象,该对象处理lambda函数(初始化程序块),根据线程执行模式(LazyThreadSafetyMode)以稍微几种不同的方式执行初始化。
@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实现的默认行为
SynchronizedLazyImpl
和
SafePublicationLazyImpl
,
UnsafeLazyImpl
通过以下过程执行延迟初始化。我们来看看前面的例子。
-
1、将传入的初始化lambda块存储在属性的
initializer
中
-
2、通过属性
_value
来存储值。此属性最开始初始值为
UNINITIALIZED_VALUE
。
-
3、在执行读取操作(属性get访问器)时,如果
_value
的值是最开始初始值
UNINITIALIZED_VALUE
,那么就会去执行
initializer
初始化器
-
4、在执行读取操作(属性get访问器)时,如果
_value
的值不是等于
UNINITIALIZED_VALUE
, 那就说明初始化操作已经执行完成了。
SynchronizedLazyImpl
如果你没有明确指定具体模式,延迟具体实现就是
SynchronizedLazyImpl
,它默认只执行一次初始化。我们来看看它的实现代码。
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的延迟初始化可以使我们更加自由地避免对线程安全性和性能的担忧。
我们还研究了延迟初始化是运算符
by
和
lazy
函数的共同作用的结果。还有更多的委托,如
Observable
和
notNull
。如有必要,你还可以实现有趣的委托属性。
读者有话说
闲聊几句,有一些小伙伴在私下问我对近期2019 Google IO 大会宣布Kotlin为Android开发首选语言,即Kotlin-First.这件事怎么看? 其实个人对于这个比较平常心,觉得不是什么特别新奇的事,说真的Kotlin自身有能力和优势充当这么个角色。如果你还在犹豫要不要学Kotlin这门语言的时候,那你就去看看Google IO使用Android代码例子几乎全是Kotlin编写,而且后面很多官方的框架和库都是Kotlin编写。咱们还是好好扎实把Kotlin中每个细节点吃透吧,那么你就会真正领会到Kotlin这门语言的设计哲学了。
说下为什么要翻译这篇文章?
这篇文章可以说把
by lazy
属性懒加载的从使用场景到原理剖析都阐述的非常清楚,并以图表形式把
by lazy
调用过程逻辑呈现得简单易懂。
你是否在Kotlin代码使用过
by lazy
呢?那你知道什么时候该使用
lateinit
,什么时候使用
by lazy
吗?实际上这篇文章已经给出答案了,这里简单大致说下:
by lazy
正如文章中说的那样用于
非空只读属性,需要延迟加载情况
,而
lateinit
一般用于
非空可变属性,需要延迟加载情况
。后续会有相关文章详细阐述它们区别以及使用场景。
其实这篇文章还涉及到一个点,那就是
by lazy
中的
SynchronizedLazyImpl
,实际上通过反编译后代码可知它是DCL(double check lock),所以可以利用
Companion Object
+
by lazy
可以实现Kotlin中的DCL单例模式。具体可参考我之前那篇文章
当Kotlin完美邂逅设计模式之单例模式(一)
Kotlin系列文章,欢迎查看:
Kotlin邂逅设计模式系列:
数据结构与算法系列:
翻译系列:
-
[译] Kotlin中关于Companion Object的那些事
-
[译]记一次Kotlin官方文档翻译的PR(内联类)
-
[译]Kotlin中内联类的自动装箱和高性能探索(二)
-
[译]Kotlin中内联类(inline class)完全解析(一)
-
[译]Kotlin的独门秘籍Reified实化类型参数(上篇)
-
[译]Kotlin泛型中何时该用类型形参约束?
-
[译] 一个简单方式教你记住Kotlin的形参和实参
-
[译]Kotlin中是应该定义函数还是定义属性?
-
[译]如何在你的Kotlin代码中移除所有的!!(非空断言)
-
[译]掌握Kotlin中的标准库函数: run、with、let、also和apply
-
[译]有关Kotlin类型别名(typealias)你需要知道的一切
-
[译]Kotlin中是应该使用序列(Sequences)还是集合(Lists)?
-
[译]Kotlin中的龟(List)兔(Sequence)赛跑
原创系列:
-
教你如何完全解析Kotlin中的注解
-
教你如何完全解析Kotlin中的类型系统
-
如何让你的回调更具Kotlin风味
-
Jetbrains开发者日见闻(三)之Kotlin1.3新特性(inline class篇)
-
JetBrains开发者日见闻(二)之Kotlin1.3的新特性(Contract契约与协程篇)
-
JetBrains开发者日见闻(一)之Kotlin/Native 尝鲜篇
-
教你如何攻克Kotlin中泛型型变的难点(实践篇)
-
教你如何攻克Kotlin中泛型型变的难点(下篇)
-
教你如何攻克Kotlin中泛型型变的难点(上篇)
-
Kotlin的独门秘籍Reified实化类型参数(下篇)
-
有关Kotlin属性代理你需要知道的一切
-
浅谈Kotlin中的Sequences源码解析
-
浅谈Kotlin中集合和函数式API完全解析-上篇
-
浅谈Kotlin语法篇之lambda编译成字节码过程完全解析
-
浅谈Kotlin语法篇之Lambda表达式完全解析
-
浅谈Kotlin语法篇之扩展函数
-
浅谈Kotlin语法篇之顶层函数、中缀调用、解构声明
-
浅谈Kotlin语法篇之如何让函数更好地调用
-
浅谈Kotlin语法篇之变量和常量
-
浅谈Kotlin语法篇之基础语法
Effective Kotlin翻译系列
-
[译]Effective Kotlin系列之考虑使用原始类型的数组优化性能(五)
-
[译]Effective Kotlin系列之使用Sequence来优化集合的操作(四)
-
[译]Effective Kotlin系列之探索高阶函数中inline修饰符(三)
-
[译]Effective Kotlin系列之遇到多个构造器参数要考虑使用构建器(二)
-
[译]Effective Kotlin系列之考虑使用静态工厂方法替代构造器(一)
实战系列: