Kotlin通常要求我们在定义属性时立即初始化它们。当我们不知道理想的初始值时,这样做似乎很奇怪,尤其是在生命周期驱动的Android属性的情况下。
幸运的是,有一种方法可以解决这个问题。如果你在没有初始化的情况下声明一个类属性,IntelliJ IDEA编辑器会警告你,并建议添加一个lateinit 的关键字。
如果一个初始化的属性或对象在程序中实际上没有被使用呢?那么,这些未使用的初始化将成为程序的负债,因为对象的创建是一个沉重的过程。这是另一个例子,说明lateinit 可以帮助我们。
本文将解释lateinit 修改器和懒惰委托是如何处理未使用或不必要的早期初始化的。这将使你的Kotlin开发工作流程更加高效。
lateinit 在Kotlin中使用
lateinit 关键字代表了 "晚期初始化"。当与类属性一起使用时,lateinit 修改器使该属性在其类的对象构造时不被初始化。
只有在程序的后期初始化时,才会为lateinit 变量分配内存,而不是在它们被声明时。这在初始化的灵活性方面是非常方便的。
让我们来看看lateinit 的一些重要特性吧!
主要特点
首先,在声明的时候,内存不会分配给lateinit 属性。初始化是在你认为合适的时候才进行的。
一个lateinit 属性在整个程序中可能会改变不止一次,而且应该是可变的。这就是为什么你应该总是把它作为一个var ,而不是作为一个val 或const 。
lateinit 初始化可以使你免于重复的空值检查,当把属性初始化为可空值类型时,你可能需要这样做。lateinit 属性的这个特性不支持可空类型。
扩展我的最后一点,lateinit 可以很好地用于非原始数据类型。它不能与原始类型如long 或int 一起使用。这是因为每当访问一个lateinit 属性时,Kotlin都会在引擎盖下为它提供一个空值,以表明该属性尚未被初始化。
原始类型不能被null ,所以没有办法表明一个未初始化的属性。因此,原始类型在与lateinit 关键字一起使用时,会出现异常。
最后,一个lateinit 属性在被访问之前必须被初始化,否则会抛出一个UninitializedPropertyAccessException 错误,如下图所示:

一个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 声明来保持该属性的可变性。使用val 和const 没有任何意义,因为它们表示不可变的属性,而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 。
每当这个TextView 在MainActivity 中第一次被调用时,一个带有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开发中的一些实际用例。
感谢你花时间把这篇入门文章读完!我希望你能用这个指南在你的应用开发之路上实现这两个功能。