小编典典

使用“by lazy”与“lateinit”进行属性初始化

all

在 Kotlin 中,如果您不想在构造函数内或类主体顶部初始化类属性,则基本上有以下两个选项(来自语言参考):

  1. 延迟初始化

lazy()是一个函数,它接受一个 lambda
并返回一个实例,Lazy<T>该实例可以作为实现惰性属性的委托:第一次调用get()执行传递给的
lambdalazy()并记住结果,随后的调用get()只是返回记住的结果。

例子

public class Hello {

   val myLazyString: String by lazy { "Hello" }

}

因此,第一次调用和后续调用,无论它在哪里,myLazyString都会返回Hello

  1. 后期初始化

通常,声明为非空类型的属性必须在构造函数中初始化。但是,这通常并不方便。例如,可以通过依赖注入来初始化属性,或者在单元测试的 setup
方法中初始化属性。在这种情况下,您不能在构造函数中提供非 null 初始化程序,但您仍然希望在引用类主体内的属性时避免 null 检查。

要处理这种情况,您可以使用 lateinit 修饰符标记属性:

public class MyTest {

   lateinit var subject: TestSubject

   @SetUp fun setup() { subject = TestSubject() }

   @Test fun test() { subject.method() }
}

修饰符只能用于在类主体内(不能在主构造函数中)声明的 var 属性,并且只能在属性没有自定义 getter 或 setter
时使用。属性的类型必须是非空的,并且不能是原始类型。

那么,既然它们都可以解决同一个问题,那么如何在这两个选项之间做出正确的选择呢?


阅读 91

收藏
2022-03-21

共1个答案

小编典典

lateinit var以下是委托属性和委托属性之间的显着差异by lazy { ... }

  • lazy { ... }delegate 只能用于val属性,而lateinit只能应用于vars,因为它不能编译为final字段,因此不能保证不变性;

  • lateinit var有一个存储值的支持字段,并by lazy { ... }创建一个委托对象,一旦计算出值就存储在其中,将对委托实例的引用存储在类对象中,并为与委托实例一起工作的属性生成 getter。因此,如果您需要类中存在的支持字段,请使用lateinit;

  • 除了vals,lateinit不能用于可空属性或 Java 原始类型(这是因为null用于未初始化的值);

  • lateinit var可以从可以看到对象的任何地方初始化,例如从框架代码内部,并且单个类的不同对象可以有多个初始化场景。by lazy { ... }反过来,定义了属性的唯一初始化程序,只能通过覆盖子类中的属性来更改它。如果您希望您的属性以事先可能未知的方式从外部初始化,请使用lateinit.

  • 初始化by lazy { ... }默认是线程安全的,并保证初始化器最多被调用一次(但这可以通过使用另一个lazy重载来改变)。在 的情况下lateinit var,由用户代码在多线程环境中正确初始化属性。

  • 一个Lazy实例可以被保存、传递,甚至可以用于多个属性。相反,lateinit vars 不存储任何额外的运行时状态(仅null在未初始化值的字段中)。

  • 如果您持有对 的实例的引用Lazy,则isInitialized()允许您检查它是否已经被初始化(并且您可以通过委托属性的反射获取此类实例)。要检查一个 lateinit 属性是否已经被初始化,你可以使用property::isInitializedKotlin 1.2 起

  • 传递给的 lambdaby lazy { ... }可以从使用它的上下文中捕获引用到它的闭包中。然后它将存储引用并仅在属性初始化后才释放它们。这可能会导致对象层次结构(例如 Android 活动)不会被释放太久(或者永远不会被释放,如果该属性仍然可以访问并且永远不会被访问),因此您应该小心在初始化程序 lambda 中使用的内容。

另外,问题中没有提到另一种方法:Delegates.notNull(),它适用于非空属性的延迟初始化,包括Java原始类型的属性。

2022-03-21