Kotlin 中的 Lazy vs Lateinit 解析
hi!Kotlin 开发爱好者们 类的属性是每个代码库的基础部分。让我们看看一些初始化它们的方法,以简化代码并提高效率和可读性!系好安全带,开车!
根据你的需求,你可能希望你的属性是不可变的 val
或可变的 var
。开发者经常在稍后在代码中初始化属性时遇到困难。这就是为什么在 Kotlin 中,我们有 lazy
和 lateinit
属性初始化器。
by lazy()
lazy
只能用于 val
。顾名思义,属性是延迟初始化的,这意味着它将在首次使用时运行初始化代码。
我们从这得到了什么?
-
我们 100% 确定值不会改变。
-
更好的资源管理。有时,你需要初始化多个属性,其中一些可能需要很长时间。这是一个考虑使用
lazy
并推迟一些工作以改善启动时间和内存分配的地方。
看看这个例子:
go
class Example {
val lazyExample by lazy { "Example" }
fun useExample() {
println(lazyExample)
}
}
fun main() {
val example = Example()
// 现在 `lazyExample` 属性尚未初始化
example.useExample()
// 现在 `lazyExample` 有了值,因为在 `useExample()` 函数中使用了它
}
fun main() {
val example = Example()
// 外部读取属性也会初始化它
println(example.lazyExample)
}
如示例所示,大多数时候,你会希望你的 lazy
是一个 lambda 函数,这是一种直观的属性初始化方式。
实际上,lazy
只是 Kotlin 委托的智能使用。默认情况下,它使用初始化器的线程安全版本。
简而言之,下面的代码有一个在开始时未初始化的内部 var
,并且在每次 get()
时都会进行检查:
go
// Kotlin 标准库代码
public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)
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
// 最终字段是启用构造实例的安全发布所必需的
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)
}
lateinit
lateinit
只能用于 var
。你可能希望属性在运行时改变,并在启动时将其留为未初始化状态。
它的工作方式与 lazy
不同。在这里,你不能 100% 确定属性是否已初始化。使用 lateinit
就像是告诉编译器你知道你在做什么,并且属性将在首次读取属性之前被初始化。否则,它将崩溃。
我们从这得到了什么?
-
虽然
lazy
只能在 lambda 中初始化;lateinit
初始化可以发生在程序的任何地方,这是瓶颈优化的好地方。 -
可变性是你想要的。
看看它是如何工作的:
go
class Example {
lateinit var lazyExample: String
fun initializeExample() {
lazyExample = "example"
}
fun useExample() {
println(lazyExample)
}
}
// 崩溃示例
fun main() {
val example = Example()
example.useExample() // CRASH: UninitializedPropertyAccessException
}
// 工作示例
fun main() {
val example = Example()
example.initializeExample() // 首先需要初始化变量
example.useExample() // 打印 example
}
实际上,你可以检查变量是否已初始化,并且只在初始化代码时运行:
go
class Example {
lateinit var lazyExample: String
fun initializeExample() {
lazyExample = "example"
}
fun useExample() {
// 在真实应用中不要这样做
if (::lazyExample.isInitialized) {
println(lazyExample)
} else {
initializeExample()
println(lazyExample)
}
}
}
然而,你绝不应该在你的程序中使用它!isInitialized
应该仅用于测试,因为它使用反射并且非常难以阅读。如果你需要在运行时检查变量是否已初始化,使用可空版本:
go
class Example {
var lazyExample: String? = null
fun initializeExample() {
lazyExample = "example"
}
fun useExample() {
if (lazyExample != null) {
println(lazyExample)
} else {
initializeExample()
println(lazyExample)
}
}
}
原始类型
通常,使用原始类型时不可能使用 lateinit
,因为 Kotlin 创建了一个存储属性的后端字段。因此,你不能使用原始类型。
相反,你应该使用 Delegates.notNull<T>()
。它的工作方式相同。唯一改变的是签名:
go
class Example {
// 用于原始类型
var lazyExample by Delegates.notNull<Int>()
fun initializeExample() {
lazyExample = 1
}
fun useExample() {
println(lazyExample)
}
}
实际上,它也被装箱,但通过委托完成。
感谢阅读!如果你学到了新东西,请关注我获取更多!