Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

延迟初始化

有时候,你想在创建类的实例后才初始化属性,但是想在一个单独的成员函数中进行初始化,而不是使用 lazy

例如,一个框架或库可能要求在特殊函数中进行初始化。如果你扩展了该库的类,你可以提供自己的特殊函数的实现。

考虑一个具有 setUp() 函数来初始化实例的 Bag 接口:

// LateInitialization/Bag.kt
package lateinitialization

interface Bag {
  fun setUp()
}

假设我们想要重用一个创建和操作 Bag 的库,并保证调用了 setUp()。该库要求在 setUp() 中进行子类初始化,而不是在构造函数中:

// LateInitialization/Suitcase.kt
package lateinitialization
import atomictest.eq

class Suitcase : Bag {
  private var items: String? = null
  override fun setUp() {
    items = "socks, jacket, laptop"
  }
  fun checkSocks(): Boolean =
    items?.contains("socks") ?: false
}

fun main() {
  val suitcase = Suitcase()
  suitcase.setUp()
  suitcase.checkSocks() eq true
}

Suitcase 通过覆盖 setUp() 来初始化 items。然而,我们不能仅仅将 items 定义为一个 String —— 如果这样做,我们必须在构造函数中提供一个非空的初始化值。使用空字符串等存根值是一个不好的做法,因为你永远不知道它是否实际被初始化过。null 表示尚未初始化。

items 定义为可为空的 String? 意味着我们必须在所有成员函数中检查 null,如 checkSocks()。然而,我们知道我们正在重用的库通过调用 setUp() 来初始化 items,因此 null 检查不应该是必要的。

lateinit 属性修饰符可以解决这个问题 —— 在创建 BetterSuitcase 实例后,我们初始化 items

// LateInitialization/BetterSuitcase.kt
package lateinitialization
import atomictest.eq

class BetterSuitcase : Bag {
  lateinit var items: String
  override fun setUp() {
    items = "socks, jacket, laptop"
  }
  fun checkSocks() = "socks" in items
}

fun main() {
  val suitcase = BetterSuitcase()
  suitcase.setUp()
  suitcase.checkSocks() eq true
}

将这个版本的 checkSocks()Suitcase.kt 中的版本进行比较。lateinit 意味着 items 被安全地定义为非空属性。

lateinit 可以用于类体内的属性、顶层属性或局部 var

限制:

  • lateinit 只能用于 var 属性,不能用于 val
  • 属性必须是非空类型。
  • 属性不能是原始类型。
  • abstract 类或 interface 中的 abstract 属性不允许使用 lateinit
  • 具有自定义 get()set() 的属性不允许使用 lateinit

如果你忘记初始化这样的属性会发生什么?你不会得到编译时的错误或警告,因为初始化逻辑可能很复杂,并且可能依赖于 Kotlin 无法监视的其他属性:

// LateInitialization/FaultySuitcase.kt
package lateinitialization
import atomictest.*

class FaultySuitcase : Bag {
  lateinit var items: String
  override fun setUp() {}
  fun checkSocks() = "socks" in items
}

fun main() {
  val suitcase = FaultySuitcase()
  suitcase.setUp()
  capture {
    suitcase.checkSocks()
  } eq
    "UninitializedPropertyAccessException" +
    ": lateinit property items " +
    "has not been initialized"
}

这个运行时异常提供了足够的细节,让你能够轻松地发现并修复问题。通常情况下,跟踪由于 null 指针异常引起的错误要困难得多。

.isInitialized 可以告诉你一个 lateinit 属性是否被初始化。该属性必须在当前作用域中,使用 :: 运算符访问:

// LateInitialization/IsInitialized.kt
package lateinitialization
import atomictest.*

class WithLate {
  lateinit var x: String
  fun status() = "${::x.isInitialized}"
}

lateinit var y: String

fun main() {
  trace("${::y.isInitialized}")
  y = "Ready"
  trace("${::y.isInitialized}")
  val withlate = WithLate()
  trace(withlate.status())
  withlate.x = "Set"
  trace(withlate.status())
  trace eq "false true false true"
}

虽然可以创建一个局部的 lateinit var,但你不能在其上调用 .isInitialized,因为不支持对局部 varval 的引用。

练习和解答可在 www.AtomicKotlin.com 找到。