Kotlin-使用“ by lazy”和“ lateinit”进行属性初始化


279

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

  1. 延迟初始化

lazy()是一个函数,它需要一个lambda并返回Lazy的一个实例,该实例可以充当实现lazy属性的委托:对get()的第一次调用执行传递给lazy()的lambda并记住结果,随后的调用get()只是返回记忆的结果。

public class Hello {

   val myLazyString: String by lazy { "Hello" }

}

因此,无论在哪里,对myLazyString的第一个调用和后续调用将返回“ Hello”

  1. 后期初始化

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

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

public class MyTest {
   
   lateinit var subject: TestSubject

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

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

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

那么,由于这两个选项都可以解决相同的问题,因此如何在这两个选项之间正确选择?

Answers:


334

以下是lateinit varby lazy { ... }委托属性之间的重大区别:

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

  • lateinit var具有一个用于存储值的后备字段,并by lazy { ... }创建一个代理对象,一旦计算出该值,该对象就会存储在其中,将对代理实例的引用存储在类对象中,并为与该代理实例一起使用的属性生成吸气剂。因此,如果您需要类中存在的后备字段,请使用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以后的版本

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

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


9
好答案!我要补充一点的是,lateinit它以设置者的可见性公开了它的支持字段,因此从Kotlin和Java访问属性的方式是不同的。从Java代码开始,甚至null在Kotlin中无需检查也可以将此属性设置为。因此lateinit,不是用于延迟初始化,而是用于不一定来自Kotlin代码的初始化。
迈克尔

有什么等效于Swift的“!” ?? 换句话说,它是后初始化的,但可以检查是否为null而不失败。如果您选中“ theObject == null”,则Kotlin的“ lateinit”将失败,并显示“ lateinit属性currentUser尚未初始化”。当您的对象在其核心使用场景中不为空(因此要针对非空的抽象进行编码),但在特殊/有限的场景中为空(即:访问当前记录的对象)时,此功能非常有用在用户中,除非是首次登录/在登录屏幕上,否则永远不会为null)
Marchy

@Marchy,您可以使用显式存储的Lazy+ .isInitialized()来实现。我猜没有一种直接的方法可以检查此类属性,null因为可以保证无法从中获取该属性null。:)观看此演示
热键

@hotkey关于使用太多by lazy会降低构建时间或运行时间吗?
Dr.jacky '19

我喜欢使用lateinit来绕过null未初始化值的使用的想法。除此之外,null永远不要使用它,并且lateinit可以消除null。那就是我爱Kotlin的方式:)
KenIchi

26

除了hotkey很好的答案,以下是我在实践中如何选择的两种方法:

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

例如,通过调用:

private lateinit var value: MyClass

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

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


1
我认为即使依赖于外部对象,我们仍然可以延迟初始化。只需要将值传递给内部变量。并在延迟初始化期间使用内部变量。但这和Lateinit一样自然。
艾里

这种方法抛出UninitializedPropertyAccessException,在使用该值之前,我再次检查了我是否正在调用setter函数。我有Lateinit我缺少的特定规则吗?在您的答案中,将MyClass和Any替换为android Context,这就是我的情况。
塔拉(Talha)

24

非常简短的答案

lateinit:最近初始化非null属性

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

延迟初始化

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

由lazy {...}进行的初始化是在定义的属性被首次使用而不是其声明的地方进行的。


很好的答案,尤其是“执行首次使用已定义属性而不是声明的初始化程序”
user1489829

17

lateinit vs懒惰

  1. 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。

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

    ii)除非使用变量,否则不会对其进行初始化。

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

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

    v)变量只能是val

    vi)该变量只能是不可为null的


7
我认为在惰性变量中不能是var。
丹麦夏尔马

4

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

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

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

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


是的,我也初始化中onCreateViewonResume和其他同lateinit,但有时会发生错误,在那里(因为一些事件起步较早)。因此也许by lazy可以给出适当的结果。我使用lateinit可以在生命周期中更改的非null变量。
CoolMind

2

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


0

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

    @Autowired
    lateinit var myBean: MyBean

1
应该像@Autowired lateinit var myBean: MyBean
Cnfn

0

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.