原文链接:https://medium.com/til-kotlin/how-kotlins-delegated-properties-and-lazy-initialization-work-552cbad8be60
访问属性在支持面向对象范式的编程语言中非常常见。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);
}
}
可以通过声明为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 } }
上面的代码运行良好,但是在使用属性之前每次检查它是否为null都有点繁琐。可以通过使用一个非null类型来忽略它(你相信)它总是有一个值。
class MainActivity: AppCompatActivity () { private var mWelcomeTextView: TextView ... }
当然,需要使用
lateinit
说明之后会给这个控件赋值
与我们通常谈论的延迟初始不同,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
。
但是,当声明只读属性时,我们面临的问题是无法定义执行初始化的位置。
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
如何工作的。
代表团的字面意思是委派。这意味着委托人可以替代原来的访问者执行一些操作。
委托属性代理了属性的getter/setter
,它允许代理对象在读写值时执行一些内部操作。
Kotlin支持将接口(Class Delegation)或访问器(Delegated Properties)的实现委托给另一个对象。更多细节将在另一篇文章中介绍。:)
)](https://cdn-images-1.medium.com/max/1600/1*EThddmBwZW8EZT-JucFHcg.jpeg)
可以紧跟by
格式声明一个代理属性:
val / var \
现在让我们再次访问该属性的代码。
我们可以认为by lazy
将一个属性作为带lazy
委托的委托属性。
因此,lazy
是如何工作的?让我们总结一下Kotlin标准库引用的lazy()
,如下所示:
lazy()
返回Lazy
示例,该示例储存了lambda初始化器。lazy()
的lambda表达式,并储存表达式的结果。简单来讲,
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
的getValue()
方法返回Lazy
实例的值。
lazy
返回Lazy
对象,该对象根据线程执行模型(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
PUBLICATION → SafePublicationLazyImpl
NONE → UnsafeLazyImpl
* 只需在第一次访问时初始化,或者返回存储的值。
* 没有考虑多线程,所以它是不安全的。
SynchronizedLazyImpl
、 SafePublicationLazyImpl
和UnsafeLazyImpl
通过以下流程执行惰性初始化。让我们看看前面的例子。
属性的initializer
存储传入的初始化lambda表达式。
通过_value
属性存储值。这个属性的 初始值是UNINITIALIZED_VALUE
。
_value
的值是UNINITIALIZED_VALUE
,则执行initializer
表达式。 _value
值不是UNINITIALIZED_VALUE
,读操作将返回_value
,因为初始化已经完成了。 如果不指定模式,惰性实现则使用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()
执行初始化块。initializer
赋值为null,因为初始化完成后不再需要它了。当然,惰性初始化有时会导致问题发生,或者在异常情况下绕过控制流并生成正常值,从而使调试变得困难。
但是,如果您对这些情况非常小心,那么Kotlin的惰性初始化可以让我们免于担心线程安全和性能问题。
我们还确认了惰性初始化是运算符和惰性函数的结果。还有更多的代理方式,比如Observable
和notNull
。如果有必要,你也可以实现有趣的委托属性。请享用吧!