Kotlin - 使用"by lazy"与"lateinit"进行属性初始化

reg*_*aes 232 properties kotlin

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

  1. 延迟初始化

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

public class Hello {

   val myLazyString: String by lazy { "Hello" }

}
Run Code Online (Sandbox Code Playgroud)

所以第一次调用和子命令调用,无论它在哪里,到myLazyString将返回"Hello"

  1. 延迟初始化

通常,必须在构造函数中初始化声明为具有非null类型的属性.但是,这通常不方便.例如,可以通过依赖注入或单元测试的设置方法初始化属性.在这种情况下,您无法在构造函数中提供非null初始值设定项,但在引用类体内的属性时仍希望避免空值检查.

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

public class MyTest {

   lateinit var subject: TestSubject

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

   @Test fun test() { subject.method() }
}
Run Code Online (Sandbox Code Playgroud)

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

那么,如何在这两个选项之间正确选择,因为它们都可以解决同样的问题?

hot*_*key 274

以下是lateinit varby lazy { ... }委托财产之间的重大差异:

  • lazy { ... }委托只能用于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::isInitialized自Kotlin 1.2以来.

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

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

  • 很棒的答案!我想补充一点,`lateinit`暴露了它的后备字段,可以看到setter的可见性,所以从Kotlin和Java访问属性的方式是不同的.从Java代码中可以将此属性设置为"null"而无需在Kotlin中进行任何检查.因此`lateinit`不是用于延迟初始化,而是用于初始化,不一定是来自Kotlin代码. (5认同)

Gee*_*pta 62

延迟初始化 vs 懒惰

  1. 延迟初始化

    i) 与可变变量 [var] 一起使用

     lateinit var name: String       //Allowed
     lateinit val name: String       //Not Allowed
    
    Run Code Online (Sandbox Code Playgroud)

ii) 仅允许不可为空的数据类型

    lateinit var name: String       //Allowed
    lateinit var name: String?      //Not Allowed
Run Code Online (Sandbox Code Playgroud)

iii) 向编译器承诺该值将在未来被初始化。

注意:如果您尝试访问lateinit变量而不初始化它,那么它会抛出 UnInitializedPropertyAccessException。

  1. 懒惰的

    i) 延迟初始化旨在防止对象的不必要初始化。

ii) 你的变量不会被初始化,除非你使用它。

iii) 它只被初始化一次。下次使用它时,您会从缓存中获取值。

iv)它是线程安全的(它在第一次使用它的线程中初始化。其他线程使用存储在缓存中的相同值)。

v) 变量只能是val

vi) 变量只能是不可为空的

  • 我认为惰性变量不能是var。 (10认同)

Gui*_*ume 19

除了hotkey好的答案之外,这里是我在实践中如何选择:

lateinit 用于外部初始化:当您需要外部资源来通过调用方法来初始化您的值时.

例如通过致电:

private lateinit var value: MyClass

fun init(externalProperties: Any) {
   value = somethingThatDependsOn(externalProperties)
}
Run Code Online (Sandbox Code Playgroud)

虽然lazy它只使用对象内部的依赖项.


Yog*_*ria 14

非常简短的答案

lateinit:它最近初始化非null属性

与延迟初始化不同,lateinit允许编译器识别非null属性的值未存储在构造函数阶段中以进行正常编译.

懒惰的初始化

当实现在Kotlin中执行延迟初始化的只读(val)属性时,通过lazy可能非常有用.

by lazy {...}执行其初始化程序,其中首先使用定义的属性,而不是其声明.

  • 很好的答案,尤其是“在首次使用定义的属性而不是其声明的地方执行其初始化程序” (2认同)

小智 10

除了所有出色的答案之外,还有一个称为延迟加载的概念:

延迟加载是计算机编程中常用的一种设计模式,用于将对象的初始化推迟到需要的时候。

正确使用它,您可以减少应用程序的加载时间。Kotlin 的实现方式是lazy()在需要时将所需的值加载到变量中。

但是当您确定一个变量不会为空或为空并且将在您使用它之前进行初始化时使用onResume()lateinit - 例如在android 的方法中 - 因此您不想将其声明为可空类型。

  • 是的,我也在`onCreateView`、`onResume`等中用`lateinit`进行了初始化,但有时会出现错误(因为有些事件较早开始)。所以也许“by Lazy”可以给出合适的结果。我将“lateinit”用于可以在生命周期中更改的非空变量。 (2认同)

小智 5

上面一切都是正确的,但一个事实简单解释 LAZY ----在某些情况下,您希望将对象实例的创建延迟到第一次使用。这种技术称为惰性初始化或惰性实例化。延迟初始化的主要目的是提高性能并减少内存占用。如果实例化您的类型的实例需要大量计算成本,并且程序最终可能不会实际使用它,您可能希望延迟甚至避免浪费 CPU 周期。


Aja*_*rma 5

顺便说一下,lateinit 和lazy 的区别

延迟初始化

  1. 仅用于可变变量,即 var 和不可为空的数据类型

lateinit var name: String //允许不可为空

  1. 您告诉编译器该值将在将来被初始化。

注意:如果您尝试访问 lateinit 变量而不初始化它,那么它会抛出 UnInitializedPropertyAccessException。

懒惰的

  1. 延迟初始化旨在防止对对象进行不必要的初始化。

  2. 除非您使用它,否则您的变量不会被初始化。

  3. 它只初始化一次。下次使用它时,您会从缓存中获取值。

  4. 它是线程安全的。

  5. 变量只能是 val 且不可为空。

干杯:)