如何在Kotlin中初始化lazy和lateinit变量(附代码实例)

3,034 阅读10分钟

Kotlin通常要求我们在定义属性时立即初始化它们。当我们不知道理想的初始值时,这样做似乎很奇怪,尤其是在生命周期驱动的Android属性的情况下。

幸运的是,有一种方法可以解决这个问题。如果你在没有初始化的情况下声明一个类属性,IntelliJ IDEA编辑器会警告你,并建议添加一个lateinit 的关键字。

如果一个初始化的属性或对象在程序中实际上没有被使用呢?那么,这些未使用的初始化将成为程序的负债,因为对象的创建是一个沉重的过程。这是另一个例子,说明lateinit 可以帮助我们。

本文将解释lateinit 修改器和懒惰委托是如何处理未使用或不必要的早期初始化的。这将使你的Kotlin开发工作流程更加高效。

lateinit 在Kotlin中使用

lateinit 关键字代表了 "晚期初始化"。当与类属性一起使用时,lateinit 修改器使该属性在其类的对象构造时不被初始化。

只有在程序的后期初始化时,才会为lateinit 变量分配内存,而不是在它们被声明时。这在初始化的灵活性方面是非常方便的。

让我们来看看lateinit 的一些重要特性吧!

主要特点

首先,在声明的时候,内存不会分配给lateinit 属性。初始化是在你认为合适的时候才进行的。

一个lateinit 属性在整个程序中可能会改变不止一次,而且应该是可变的。这就是为什么你应该总是把它作为一个var ,而不是作为一个valconst

lateinit 初始化可以使你免于重复的空值检查,当把属性初始化为可空值类型时,你可能需要这样做。lateinit 属性的这个特性不支持可空类型。

扩展我的最后一点,lateinit 可以很好地用于非原始数据类型。它不能与原始类型如longint 一起使用。这是因为每当访问一个lateinit 属性时,Kotlin都会在引擎盖下为它提供一个空值,以表明该属性尚未被初始化。

原始类型不能被null ,所以没有办法表明一个未初始化的属性。因此,原始类型在与lateinit 关键字一起使用时,会出现异常。

最后,一个lateinit 属性在被访问之前必须被初始化,否则会抛出一个UninitializedPropertyAccessException 错误,如下图所示:

Uninitialized Property Access Exception Error

一个lateinit 属性在初始化前被访问会导致这个异常。

Kotlin允许你检查一个lateinit 属性是否被初始化。这对于处理我们刚才讨论的未初始化的异常是很方便的:

lateinit var myLateInitVar: String
...

if(::myLateInitVar.isInitialized) {
  // Do something
}

lateinit 修饰符的使用实例

让我们通过一个简单的例子来看看lateinit 修改器的使用情况。下面的代码定义了一个类,并用假值和空值初始化了它的一些属性:

class TwoRandomFruits {
  var fruit1: String = "tomato" 
  var fruit2: String? = null


  fun randomizeMyFruits() {
      fruit1 = randomFruits()
      fruit2 = possiblyNullRandomFruits()
  }


  fun randomFruits(): String { ... }
  fun possiblyNullRandomFruits(): String? { ... }
}

fun main() {
    val rf= RandomFruits()
    rf.randomizeMyFruits()


    println(rf.fruit1.capitalize())
    println(rf.fruit2?.capitalize()) // Null-check
}

这并不是初始化变量的最好方法,但在这种情况下,它仍然能完成任务。

正如你在上面看到的,如果你选择使属性为空,那么每当你修改或使用它时,你就必须对它进行空检查。这可能是相当乏味和烦人的。

让我们用lateinit 修改器来解决这个问题:

class TwoRandomFruits {
  lateinit var fruit1: String // No initial dummy value needed
  lateinit var fruit2: String // Nullable type isn't supported here


  fun randomizeMyFruits() {
      fruit1 = randomFruits()
      fruit2 = when {
          possiblyNullRandomFruits() == null -> "Tomato" // Handling null values
          else -> possiblyNullRandomFruits()!!
      }
  }


  fun randomFruits(): String { ... }
  fun possiblyNullRandomFruits(): String? { ... }
}

fun main() {
    val rf= RandomFruits()
    rf.randomizeMyFruits()


    println(rf.fruit1.capitalize())
    println(rf.fruit2.capitalize())
}

lateinit 的实现不言而喻,它展示了一种处理变量的巧妙方法除了lateinit 的默认行为之外,这里的主要收获是我们可以很容易地避免使用nullable类型。

生命周期驱动的属性和lateinit

数据绑定是另一个使用lateinit,在以后初始化活动的例子。开发人员经常希望早点初始化绑定变量,以便在其他方法中把它作为访问不同视图的引用。

在下面的MainActivity 类中,我们用lateinit 修改器声明了绑定,以实现同样的事情:

package com.test.lateinit

import androidx.appcompat.app.AppCompatActivity
import ...

class MainActivity : AppCompatActivity() {
  lateinit var binding: ActivityMainBinding

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

    ...
  }
  ...
}

MainActivity 的绑定只有在活动生命周期函数onCreate() 被触发后才能被初始化。因此,用lateinit 修饰符声明绑定在这里是完全合理的。

何时使用lateinit

在常规的变量初始化中,你必须添加一个假值,而且很可能是一个空值。这将在每次访问时增加大量的空值检查:

// Traditional initialization
var name: String? = null
...    
name = getDataFromSomeAPI()
...
// A null-check will be required whenever `name` is accessed.
name?.let { it-> 
  println(it.uppercase())
}

// Lateinit initialization
lateinit var name: String
...
name = getDatafromSomeAPI()
...
println(name.uppercase())

我们可以使用lateinit 修改器来避免这些重复的空值检查,特别是当一个属性可能经常波动的时候。

使用时要记住的事情lateinit

最好记住在访问lateinit 属性之前总是对其进行初始化,否则,你会看到在编译时抛出一个大的异常。

确保通过使用var 声明来保持该属性的可变性。使用valconst 没有任何意义,因为它们表示不可变的属性,而lateinit 将无法工作。

最后,当给定的属性的数据类型是原始的或者出现空值的可能性很大时,要避免使用lateinit 。它不是为这些情况设计的,并且不支持原始或可忽略的类型。

Kotlin中的懒惰委托

顾名思义,Kotlin的lazy ,以一种懒惰的方式初始化一个属性。本质上,它创建了一个引用,但只有在第一次使用或调用该属性时才进行初始化。

现在,你可能会问这和常规的初始化有什么不同。好吧,在构建类对象的时候,其所有的公共和私有属性都在其构造函数中被初始化。在一个类中初始化变量会有一些开销;变量越多,开销就越大。

让我们通过一个例子来理解它:

class X {
  fun doThis() {}
}

class Y {
  val shouldIdoThis: Boolean = SomeAPI.guide()
  val x = X()

  if(shouldIdoThis) {
    x.doThis()
  }
  ...
}

尽管没有使用它,但上面代码中的类Y ,仍然有一个类X 的对象被创建。类X ,如果它是一个大量构建的类,也会拖累Y

不必要的对象创建是低效的,可能会拖慢当前的类。可能是在某些条件下不需要一些属性或对象,这取决于程序流程。

也可能是属性或对象依赖于其他属性或对象的创建。懒惰委托有效地处理了这两种可能性。

关键特征

一个具有懒惰初始化的变量在被调用或使用之前不会被初始化。这样一来,变量只被初始化一次,然后它的值被缓存起来,以便在程序中进一步使用。

由于用懒惰委托初始化的属性应该自始至终使用相同的值,它具有不可改变的性质,一般用于只读属性。你必须用一个val 声明来标记它。

它是线程安全的,即只计算一次,并默认由所有线程共享。一旦被初始化,它就会在整个程序中记住或缓存初始化值。

lateinit 相比,懒惰委托支持一个 自定义的setter和getter,允许它在读写值时进行中间操作。

使用中的懒惰委托的例子

下面的代码实现了简单的数学运算来计算某些图形的面积。在圆的情况下,计算需要一个常量值pi

class Area {
  val pi: Float = 3.14f


  fun circle(radius: Int): Float = pi * radius * radius
  fun rectangle(length: Int, breadth: Int = length): Int = length * breadth
  fun triangle(base: Int, height: Int): Float = base * height * .5f
}

fun main() {
  val area = Area()
  val squareSideLength = 51


  println("Area of our rectangle is ${area.rectangle(squareSideLength)}")
}

正如你在上面看到的,没有完成对任何圆的面积的计算,这使得我们对pi 的定义毫无用处。属性pi 仍然被初始化并分配内存。

让我们用懒惰的委托来纠正这个问题:

class Area {
  val pi: Float by lazy {
    3.14f
  } 


  fun circle(...) = ...
  fun rectangle(...) = ...
  fun triangle(...) = ...
}

fun main() {
  val area = Area()
  val squareSideLength = 51
  val circleRadius = 37

  println("Area of our rectangle is ${area.rectangle(squareSideLength)}")
  println("Area of our circle is ${area.circle(circleRadius)}")
}

上述懒惰委托的实现只在访问pi 的时候才使用它。一旦被访问,它的值就被缓存起来,并保留在整个程序中使用。我们将在接下来的例子中看到它在对象中的作用。

中级动作

下面是你如何在通过懒惰委托写值时添加一些中间动作。下面的代码lazy ,在一个Android活动中初始化了一个TextView

每当这个TextViewMainActivity 中第一次被调用时,一个带有LazyInit 标签的调试消息就会被记录下来,如下图中委托的lambda函数:

...
class MainActivity : AppCompatActivity() {
  override fun onCreate(...) {
    ...  
    val sampleTextView: TextView by lazy {
      Log.d("LazyInit", "sampleTextView")
      findViewById(R.id.sampleTextView)
    }
  }
  ...
}

Android应用程序中的懒惰委托

现在让我们继续讨论懒惰委托在Android应用程序中的应用。最简单的用例可以是我们之前的例子,一个有条件地使用和操作视图的Android活动:

package com.test.lazy

import androidx.appcompat.app.AppCompatActivity
import ...

class MainActivity : AppCompatActivity() {
  lateinit var binding: ActivityMainBinding

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = DataBindingUtil.setContentView(this, R.layout.activity_main)


    val sharedPrefs by lazy {
      activity?
        .getPreferences(Context.MODE_PRIVATE)
    } 


    val startButton by lazy {
      binding.startButton
    }


    if(sharedPrefs.getBoolean("firstUse", true)) {
      startButton.isVisible = true
      startButton.setOnClickListener {
        // Finish onboarding, move to main screen; something like that
        sharedPrefs.setBoolean("firstUse", false)
      }
    }

  }
}

上面,我们用懒惰委托初始化了SharedPreferences 和一个Button 。该逻辑需要根据从共享偏好中获取的布尔值来实现一个入职屏幕。

by lazy 和 之间的区别。= lazy

by lazy 语句通过懒惰委托直接向一个给定的属性添加增强。它的初始化将只在其第一次访问时发生一次:

val prop by lazy {
  ...
}

另一方面,= lazy 语句持有一个对委托对象的引用,通过它你可以使用isInitialized() 委托方法或用value 属性访问它:

val prop = lazy {
  ...
}
...

if(prop.isInitialized()) {
  println(prop.value)
}

何时使用lazy

考虑使用懒惰委托来减轻一个涉及到其他类对象的多重和/或有条件创建的类。如果对象的创建依赖于类的内部属性,那么懒惰委托就是最好的方法:

class Employee {
    ...
    fun showDetails(id: Int): List<Any> {
        val employeeRecords by lazy {
            EmployeeRecords(id) // Object's dependency on an internal property
        }
    }
    ...
}

使用时要记住的事情lazy

懒惰初始化是一种委托,它只初始化一次东西,而且只在它被调用的时候。它是为了避免不必要的对象创建。

委托对象缓存了第一次访问时返回的值。这个缓存的值在程序中需要时被进一步使用。

你可以利用它的自定义getter和setter在读写值时进行中间操作。我也更喜欢将其用于不可变类型,因为我觉得它在整个程序中保持不变的情况下效果最好。

总结

在这篇文章中,我们讨论了Kotlin的lateinit 修改器和懒惰委托。我们展示了一些基本的例子,证明了它们的用途,还谈到了Android开发中的一些实际用例。

感谢你花时间把这篇入门文章读完!我希望你能用这个指南在你的应用开发之路上实现这两个功能。