Lazy vs Lateinit vs Nullable
在本博客中,我们将介绍Kotlin提供的不同选项,以实现延迟初始化模式。我们将指出如何使用它们以及选择哪些。
Kotlin提供了三种内置方法来实现此模式:

1、什么是Lazy模式 Lazy初始化模式,也称为延迟初始化,用于将对象的创建推迟到到稍后的时间点。在大多数情况下,成员变量是在创建其父对象时初始化的。然而,在某些情况下,将创建推迟到稍后时间是有利的。例如,如果创建对象需要花费大量时间,并且将其推迟到使用它的实际时间点是有意义的,则可能会发生这种情况。另一个原因可能是在创建父对象时我们无法访问具体对象。例如,一个Android应用程序的Activity类是一个例子。
2、通过Lazy委托
Kotlin提供了一个预构建属性委托,可以包装任何对象或成员变量。如果您不确定如何在Kotlin中使用委托,我下一篇文章会介绍。
使用此方法的缺点是,无法重新分配此委托包装的成员。问题是Lazy
没有实现该setValue
功能。val 它只能在第一次分配期间用于只读。即使您将确实的函数实现为扩展函数,缓存的值也是类型val。
以下代码显示了一个简单的用例。是lazyVal
在第一次访问时分配的。
kotlin
class LazyExample {
val lazyVal: Int by lazy {
println("LazyVal init")
1
}
init {
println("LazyExample init")
}
}
fun main() {
val lazy = LazyExample()
println(lazy.lazyVal)
println(lazy.lazyVal)
}
该代码的输出如下。正如您所看到的,委托的函数体仅评估一次。然后它使用缓存的值。
csharp
LazyExample init
LazyVal init
1
1
2.1、线程安全
要创建新的Lazy
对象,您必须使用特定的初始化函数initalizer
。默认情况下,该函数是线程安全的。请注意,返回的实例使用自身进行同步。如果尝试从外部代码同步以同步包装的成员变量,可能会导致死锁。
以下代码取自原生Kotlin API。它显示了创建线程(不)安全代码的不同选项。您还可以看到它可能会抛出异常。我们建议使用标准模式(线程安全)以避免任何冲突。
kotlin
public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
when(mode) {
LazyThreadSafetyMode.SYNCHRONIZED -> if (isExperimentalMM()) SynchronizedLazyImpl(initializer) else throw UnsuppoetedOperationException()
LazyThreadSafetyMode.PUBLICATION -> if (isExperimentalMM()) SafePublicationLazyImpl(initializer) else FreezeAwareLazyImpl(initializer)
LazyThreadSafeMode.NONE -> UnsafeLazyImpl(iniitializer)
}
2.2、优点和缺点
优点:
- 现成安全
- 无需检查是否已初始化
缺点:
- 只读(标准实现)
3、Lateinit关键字
该lateinit
关键字不能用于基本类型。这意味着为了使用它,你必须有一个适当的类。实际上,这不是问题,因为基本类型不太可能用延迟初始化。此外,它只能用于可变类型。换句话说,对于变量(var)。
3.1、Android示例
这种方法经常在Android类中Activity中使用。通常,在此类中,您提供对XML文件中定义的UI元素的绑定。但你只能在Activity里特殊的方法onCreate()
之后才能访问这些对象。因此,您被迫延迟分配按钮等,直到您可以访问UI元素。
kotlin
class MainActivity: AppCompatActivity() {
private lateinit var button: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button = findViewById(R.id.myButton)
}
}
3.2、检查是否初始化
这个关键字的一个大问题是,你的代码中有一个潜在的异常。您可以在实际分配变量之前访问该lateinit
变量(编译器不会报错)。但这会抛出异常。
kotlin
class LateInitExamplt {
lateinit var lateInit: NotPrimitive
fun isInit(): Boolean {
return this::lateInit.isInitialized
}
}
fun main() {
var obj = LateInitExample()
if (obj.isInit()) {
println(obj.lateInit)
}
}
3.3、线程安全
这种方法不是线程安全的。您必须确保在多线程的情况下正确处理初始化。
3.4、优点和缺点
优点:
- 易于使用(无开销)
- 适用于var和val
缺点:
- 线程不安全
- 需要检查(确保)它已初始化
- 不适用于原始类型
4、Nullable对象
默认情况下,Kotlin中的每个(成员)变量都必须为非空。然而这个约束可以被削弱来实现nullable
。从某种意义上说,如果我们能消除这个现实,Kotlin的行为就会更像Java代码。
以下示例延时如何使用可为null的对象。通过使用"?"对象上的修饰符被声明为可为空。它可以具有"null"状态。
kotlin
class NullableObject {
var nullable: Int? = null
}
fun main() {
var obj = NullableObject()
obj.nullable = 2
if (obj.nullable != null) {
println(obj.nullable!!)
}
}
4.1、线程安全
默认情况下,这种方法不是线程安全的。在多线程环境中使用它时,你必须确保它已正确初始化。
4.2、检查是否不为空
该方法有一个大问题是存在出现NullPointerException
的潜在风险。因此,您需要检查对象是否为空(参见上面代码)。Kotlin以一种方式帮助您,您需要显示指示该变量是否可以安全使用。这个检查是在编译时完成,只是一个让你思考的帮助器(使用!!
)。
4.3、优点和缺点
优点:
- 适用于var和val
- 适用于各种类型
- 变量的额外状态
缺点:
- 线程不安全
- 存在潜在的异常
- 修改编写代码(
?
和!!
)
5、总结
我们看到所有方法都有优点和缺点。
我们不建议使用nullable
类型。Kotlin的var
和val
关键字可以避免使用异常,这是一种非常好的做法。如果移除这个约束,可能会导致代码的可读性和可维护性下降。
我们建议lateinit
在初始化父对象时无法访问该对象的用例中使用关键字(如Activity里面View控件的初始化)
对于所有其他用例,我们建议使用Lazy
委托。优点是内置线程安全。