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

Posted

技术标签:

【中文标题】使用“by lazy”与“lateinit”进行属性初始化【英文标题】:Property initialization using "by lazy" vs. "lateinit" 【发布时间】:2016-08-06 00:16:51 【问题描述】:

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

    Lazy Initialization

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

示例

public class Hello 

   val myLazyString: String by lazy  "Hello" 


因此,对myLazyString 的第一次调用和后续调用将返回Hello

    Late Initialization

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

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

public class MyTest 
   
   lateinit var subject: TestSubject

   @SetUp fun setup()  subject = TestSubject() 

   @Test fun test()  subject.method() 

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

那么,既然这两个选项都可以解决同一个问题,那么如何正确选择呢?

【问题讨论】:

【参考方案1】:

以下是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 ... 默认是线程安全的,并保证初始化器最多被调用一次(但这可以通过使用another lazy overload来改变)。在lateinit var 的情况下,在多线程环境中正确初始化属性取决于用户的代码。

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

如果您持有对Lazy 实例的引用,isInitialized() 允许您检查它是否已经被初始化(并且您可以通过委托属性obtain such instance with reflection)。查看lateinit属性是否已经初始化,可以use property::isInitialized since Kotlin 1.2。

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

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

【讨论】:

很好的答案!我要补充一点,lateinit 通过设置器的可见性公开其支持字段,因此从 Kotlin 和 Java 访问属性的方式是不同的。在 Java 代码中,这个属性甚至可以设置为 null,而无需在 Kotlin 中进行任何检查。因此lateinit 不是用于延迟初始化,而是用于不一定来自 Kotlin 代码的初始化。 有什么相当于 Swift 的“!” ??换句话说,它是后期初始化的,但可以检查 null 而不会失败。如果您检查 'theObject == null',Kotlin 的 'lateinit' 会失败并显示“lateinit 属性 currentUser 尚未初始化”。当您有一个在其核心使用场景中不为空的对象(因此想要针对它非空的抽象进行编码)但在异常/有限场景中为空(即:访问当前记录的在用户中,除非在初始登录/登录屏幕上,否则永远不会为空) @Marchy,您可以使用显式存储的Lazy + .isInitialized() 来做到这一点。我想没有直接的方法可以检查null 的此类属性,因为保证您无法从中获得null。 :) 见this demo。 我喜欢使用lateinit 来规避使用null 来获取未初始化值的想法。除此之外,永远不要使用null,并且使用lateinit 可以消除空值。这就是我喜欢 Kotlin 的方式:) 不是property::isInitialized,而是::property.isInitialized【参考方案2】:

lateinit 与懒惰

    延迟初始化

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

     lateinit var name: String       //Allowed
     lateinit val name: String       //Not Allowed
    

ii) 只允许使用不可为空的数据类型

    lateinit var name: String       //Allowed
    lateinit var name: String?      //Not Allowed

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

注意:如果您尝试访问 lateinit 变量而不对其进行初始化,则会引发 UnInitializedPropertyAccessException。

    懒惰

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

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

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

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

v) 变量只能是val

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

【讨论】:

我认为在惰性变量中不能是 var。【参考方案3】:

除了hotkey 的好答案,我在实践中是这样选择的:

lateinit 用于外部初始化:当您需要外部东西通过调用方法来初始化您的值时。

例如通过调用:

private lateinit var value: MyClass

fun init(externalProperties: Any) 
   value = somethingThatDependsOn(externalProperties)

lazy 仅使用对象内部的依赖项。

【讨论】:

我认为我们仍然可以延迟初始化,即使它依赖于外部对象。只需要将值传递给内部变量。并在延迟初始化期间使用内部变量。但它和 Lateinit 一样自然。 这种方法会抛出 UninitializedPropertyAccessException,我在使用该值之前仔细检查了我是否调用了 setter 函数。 lateinit 是否缺少特定规则?在您的回答中,将 MyClass 和 Any 替换为 android Context,这就是我的情况。【参考方案4】:

非常简短的回答

lateinit:最近初始化非空属性

与惰性初始化不同,lateinit可以让编译器识别出非空属性的值没有存储在构造函数阶段以正常编译。

延迟初始化

bylazy 在实现在 Kotlin 中执行延迟初始化的 只读(val) 属性时可能非常有用。

bylazy ... 在第一次使用定义的属性时执行其初始化程序,而不是其声明。

【讨论】:

很好的答案,尤其是“在首次使用定义的属性时执行其初始化程序,而不是其声明”【参考方案5】:

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

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

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

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

【讨论】:

是的,我也在onCreateViewonResume和其他用lateinit初始化,但有时会出现错误(因为一些事件开始得更早)。所以也许by lazy 可以给出适当的结果。我将lateinit 用于可以在生命周期中更改的非空变量。【参考方案6】:

区分延迟初始化和惰性

延迟初始化

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

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

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

注意:如果您尝试访问 lateinit 变量而不对其进行初始化,则会引发 UnInitializedPropertyAccessException。

懒惰

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

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

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

    它是线程安全的。

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

干杯:)

【讨论】:

这与@Geeta Gupta 的回答几乎完全相同。【参考方案7】:

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

【讨论】:

【参考方案8】:

LateinitLazy 初始化是 Kotlin 语言中的两个初始化属性。

何时使用 Lateinit

延迟初始化变量。当你确定要初始化一个 变量在使用之前。使用 var 关键字。 如果变量在稍后阶段发生变化,即,如果变量是可变的。 Lateinit 变量可以在类中声明。 Lateinit 在初始化之前不分配内存。

使用 Lateinit 时要避免什么

在使用 Lateinit 时,变量不能为 null 类型。

Lateinit 不能用于非原始数据类型,即 Long 和 诠释。

如果你尝试在没有初始化的情况下访问 Lateinit 变量,它会 抛出一个异常,说明它没有初始化或正确 正在访问。

它可以稍后被初始化

private lateinit var lateUri : Uri

何时使用延迟初始化

在惰性初始化中,你的变量不会被初始化 除非你调用/使用它。

延迟初始化将变量初始化一次;同样的 然后在整个代码中使用值。

它用于只读属性,因为相同的值变量是 贯穿始终。

此初始化用于 val 属性的情况。

当变量只被所有人共享时,这是首选 初始化一次。

当一个对象依赖于一个内部变量时可以使用它 班级。

使用延迟初始化时要避免什么

代码在一个不确定的时间传播到整个班级,这可以 导致混乱。

Lazy 对象在以下情况下返回先前初始化的值 稍后访问。

延迟初始化在用于保留的对象时会导致内存泄漏 片段,因为它包含对旧视图的引用。

val string: String by lazy val text = "this value"

延迟初始化与延迟初始化

如果属性没有自定义 setter 和 getter,Lateinit 用来。在多线程环境中,Lateinit 初始化是 取决于用户。 延迟初始化是线程安全的。 Lateinit 只能与 var 一起使用。 延迟初始化与 val 属性一起使用。

【讨论】:

【参考方案9】:

如果你正在使用 Spring 容器并且你想初始化不可为空的 bean 字段,lateinit 更适合。

    @Autowired
    lateinit var myBean: MyBean

【讨论】:

应该像@Autowired lateinit var myBean: MyBean【参考方案10】:

如果使用不可更改的变量,则最好使用by lazy ... val 进行初始化。在这种情况下,您可以确保它始终会在需要时被初始化,并且最多 1 次。

如果您想要一个可以更改其值的非空变量,请使用lateinit var。在 Android 开发中,您可以稍后在 onCreateonResume 等事件中对其进行初始化。请注意,如果您调用 REST 请求并访问此变量,可能会导致异常 UninitializedPropertyAccessException: lateinit property yourVariable has not been initialized,因为请求的执行速度比该变量初始化的速度要快。

【讨论】:

以上是关于使用“by lazy”与“lateinit”进行属性初始化的主要内容,如果未能解决你的问题,请参考以下文章

lateinit by lazy

定义全局 android.widget var/val 时的“lateinit”或“by lazy”

Kotlin基础 关键字:lateinit和by lazy

kotlin的by lazy

Kotlin类的初始化 ④ ( lateinit 延迟初始化 | ::属性名称.isInitialized 检查属性是否初始化 | lazy 惰性初始化 )

Kotlin类的初始化 ④ ( lateinit 延迟初始化 | ::属性名称.isInitialized 检查属性是否初始化 | lazy 惰性初始化 )