Kotlin的属性委托和惰性初始化是如何工作
访问属性在支持面向对象范式的编程语言中非常常见。Kotlin也提供了许多类似的方法,by lazy
进行惰性初始化就是一个很好的例子
在本文中,我们将看看如何使用Kotlin的委托来处理属性,以及by lazy
的惰性初始化,然后深入了解它们的工作方式。
Nullable 类型
我认为你们中的许多人可能已经知道nullable
,但让我们再来看看它。使用Kotlin的Android组件代码可以写成:
class MainActivity : AppCompatActivity() {
private var helloMessage : String = "Hello"
}
在自己的生命周期初始化和Nullable类型
在上面的例子中,如果在创建对象时可以进行初始化,没有什么大问题。但是,如果在特定的初始化过程之后引用它,则不能预先声明和使用某个值,因为它有自己的生命周期来初始化自己。
我们来看看一些熟悉的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);
}
}
可以通过声明为nullable类型来编写Kotlin代码,如下所示。
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
}
}
Non-null类型
上面的代码运行良好,但是在使用属性之前每次检查它是否为null都有点繁琐。可以通过使用一个非null类型来忽略它(你相信)它总是有一个值。
class MainActivity: AppCompatActivity () {
private var mWelcomeTextView: TextView
...
}
当然,需要使用
lateinit
说明之后会给这个控件赋值
lateinit:我将之后初始化非null属性
与我们通常谈论的延迟初始不同,lateinit
允许编译器识别非null属性的值未存储在构造器阶段中以便正常编译。
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应用程序中,大多数控件引用在activity的生命周期中保持不变。 换句话说,这意味着很少需要改变分配的引用。
在这一点上,我们可以轻松有以下想法:
“如果属性的值通常保存在组件的生命周期中,那么保持该值的只读类型是否足够?”
我想是这样。要做到这一点,乍一看,只需要一点努力就可以用val
替换var
。
只读属性的非null的困境
但是,当声明只读属性时,我们面临的问题是无法定义执行初始化的位置。
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
}
}
现在让我们试着解决最后的问题:
“如何使用之后分配只读属性”
惰性初始化
在Kotlin中,对只读属性执行延迟初始化时by lazy
可能非常有用。
by lazy { ... }
在属性第一次使用时执行初始化,而不是声明时。
class MainActivity : AppCompatActivity() {
private val messageView : TextView by lazy {
// runs on first access of messageView
findViewById(R.id.message_view) as TextView
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
fun onSayHello() {
// Initialization would be run at here!!
messageView.text = "Hello"
}
}
现在,我们可以声明一个只读属性,而不用担心messageView
的初始化位置。让我们看看by lazy
如何工作的。
代理属性 101
代表团的字面意思是委派。这意味着委托人可以替代原来的访问者执行一些操作。
委托属性代理了属性的getter/setter
,它允许代理对象在读写值时执行一些内部操作。
Kotlin支持将接口(Class Delegation)或访问器(Delegated Properties)的实现委托给另一个对象。更多细节将在另一篇文章中介绍。:)
)](https://cdn-images-1.medium.com/max/1600/1*EThddmBwZW8EZT-JucFHcg.jpeg)
可以紧跟by <delegate>
格式声明一个代理属性:
val / var \
`by lazy`如何工作的
现在让我们再次访问该属性的代码。
我们可以认为by lazy
将一个属性作为带lazy
委托的委托属性。
因此,lazy
是如何工作的?让我们总结一下Kotlin标准库引用的lazy()
,如下所示:
lazy()
返回Lazy<T>
示例,该示例储存了lambda初始化器。- 第一次调用getter执行传递给
lazy()
的lambda表达式,并储存表达式的结果。 - 之后,getter返回储存的值。
简单来讲,
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 <b>String getMyName()</b> {
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
的getValue()
方法返回Lazy
实例的值。
Lazy实现
lazy
返回Lazy<T>
对象,该对象根据线程执行模型(LazyThreadSafetyMode)以些许不同的方式调用lambda方法(初始化器)执行初始化操作。
@kotlin.jvm.JvmVersion
public fun lazy(
mode: LazyThreadSafetyMode,
initializer: () -> T
): Lazy =
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
通过以下流程执行惰性初始化。让我们看看前面的例子。
属性的
initializer
存储传入的初始化lambda表达式。通过
_value
属性存储值。这个属性的 初始值是UNINITIALIZED_VALUE
。- 如果读操作时,
_value
的值是UNINITIALIZED_VALUE
,则执行initializer
表达式。
- 如果
_value
值不是UNINITIALIZED_VALUE
,读操作将返回_value
,因为初始化已经完成了。
SynchronizedLazyImpl
如果不指定模式,惰性实现则使用SynchronizedLazyImpl
,该模式只执行一次初始化操作。让我们看看它的实现代码。
private object UNINITIALIZED_VALUE
private class SynchronizedLazyImpl(
initializer: () -> T,
lock: Any? = null
) : Lazy,
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()
执行初始化块。 - 由于其他线程可能已经完成了初始化操作,因此它执行了双重检查。如果初始化已经完成,它直接返回存储的值。
- 如果没有初始化,他讲执行lambda表达式并存储返回值。接着
initializer
赋值为null,因为初始化完成后不再需要它了。
Kotlin的代理属性
当然,惰性初始化有时会导致问题发生,或者在异常情况下绕过控制流并生成正常值,从而使调试变得困难。
但是,如果您对这些情况非常小心,那么Kotlin的惰性初始化可以让我们免于担心线程安全和性能问题。
我们还确认了惰性初始化是运算符和惰性函数的结果。还有更多的代理方式,比如Observable
和notNull
。如果有必要,你也可以实现有趣的委托属性。请享用吧!