Kotlin 中的初始化lazy和变量lateinit

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

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

如果初始化的属性或对象实际上并没有在程序中使用怎么办?好吧,这些未使用的初始化将成为程序的责任,因为对象创建是一个繁重的过程。这是另一个lateinit可以帮助我们的例子。

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

  • lateinit在科特林

    • 主要特征

    • lateinit使用中的修饰符示例

    • 生命周期驱动的属性和lateinit

    • 何时使用lateinit

    • 使用时要记住的事情lateinit

  • Kotlin 中的懒惰代表团

    • 主要特征

    • 使用惰性委托的示例

    • 中间动作

    • Android 应用程序中的延迟委托

    • 和之间的区别by lazy``= lazy

    • 何时使用惰性

    • 使用时要记住的事情lazy

lateinit在科特林

lateinit关键字代表“后期初始化” 。当与类属性一起使用时,lateinit修饰符会阻止该属性在其类的对象构造时被初始化。

lateinit仅当变量稍后在程序中初始化时才分配内存,而不是在声明它们时。这在初始化的灵活性方面非常方便。

让我们看看必须提供的一些重要功能lateinit!

主要特征

lateinit首先,在声明时没有为属性分配内存。当您认为合适时,稍后会进行初始化。

一个lateinit属性可能在整个程序中多次更改,并且应该是可变的。这就是为什么您应该始终将其声明为 avar而不是 a valor const。

初始化可以使您免于在将属性初始化为可空类型时可能需要的lateinit重复空检查。属性的这个特性lateinit不支持可空类型。


超过 20 万开发人员使用 LogRocket 来创造更好的数字体验了解更多 →


扩展我的最后一点,lateinit可以很好地用于非原始数据类型。它不适用于像longor之类的原始类型int。这是因为每当lateinit访问一个属性时,Kotlin 都会在后台为其提供一个空值,以指示该属性尚未初始化。

原始类型不能是null,因此无法指示未初始化的属性。lateinit因此,原始类型在与关键字一起使用时会引发异常。

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

在初始化之前访问的lateinit属性会导致此异常。

Kotlin 允许您检查lateinit属性是否已初始化。这可以很方便地处理我们刚刚讨论的未初始化异常。

lateinit var myLateInitVar: String
...
​
if(::myLateInitVar.isInitialized) {
  // Do something
}

lateinit使用中的修饰符示例

lateinit让我们通过一个简单的例子来看看修饰符的作用。下面的代码定义了一个类,并用 dummy 和 null 值初始化了它的一些属性。

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
}

这不是初始化变量的最佳方法,但在这种情况下,它仍然可以完成工作。

正如您在上面看到的,如果您选择使属性可以为空,则无论何时修改或使用它都必须对其进行空检查。这可能相当乏味和烦人。


来自 LogRocket 的更多精彩文章:

  • 不要错过来自 LogRocket 的精选时事通讯The Replay

  • 了解LogRocket 的 Galileo 如何消除噪音以主动解决应用程序中的问题

  • 使用 React 的 useEffect优化应用程序的性能

  • 在多个 Node 版本之间切换

  • 了解如何使用 AnimXYZ 为您的 React 应用程序制作动画

  • 探索 Tauri,一个用于构建二进制文件的新框架

  • 比较NestJS 与 Express.js


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,这里的主要内容是我们可以多么容易地避免使用可空类型。

生命周期驱动的属性和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声明来保持属性可变。使用valandconst没有任何意义,因为它们表示不可变的属性lateinit将不起作用。

最后,避免lateinit在给定属性的数据类型是原始数据类型或空值的可能性很高时使用。它不适用于这些情况,并且不支持原始类型或可为空的类型。

Kotlin 中的懒惰代表团

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

现在,您可能会问这与常规初始化有何不同。好吧,在类对象构造时,它的所有公共和私有属性都在其构造函数中初始化。初始化类中的变量会产生一些开销;变量越多,开销就越大。

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

class X {
  fun doThis() {}
}
​
class Y {
  val shouldIdoThis: Boolean = SomeAPI.guide()
  val x = X()
​
  if(shouldIdoThis) {
    x.doThis()
  }
  ...
}

尽管没有使用它,Y上面代码中的 class 仍然有一个由 class 创建的对象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仅在访问时使用。一旦被访问,它的值就会被缓存并保留在整个程序中使用。我们将在下一个示例中看到它与对象的作用。

中间动作

以下是在通过惰性委托写入值时如何添加一些中间操作的方法。以下代码在 Android 活动中lazy初始化 a 。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)
      }
    }

  }
}

上面,我们用惰性委托初始化了SharedPreferencesand a 。Button该逻辑需要基于从共享偏好中获取的布尔值来实现入职屏幕。

和之间的区别by lazy``= lazy

该语句将惰性委托直接添加到给定属性的增强。它的初始化只会在第一次访问时发生一次。by lazy

val prop by lazy {
  ...
}

另一方面,该语句包含对委托对象的引用,您可以通过它使用委托方法或使用属性访问它。= lazyisInitialized()value

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

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

您可以在此处查看上述代码的快速演示。

何时使用lazy

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

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

使用时要记住的事情lazy

延迟初始化是一个委托,它只初始化一次并且只在它被调用时初始化。这是为了避免不必要的对象创建。

委托对象缓存第一次访问时返回的值。需要时,此缓存值将在程序中进一步使用。

在读取和写入值时,您可以利用其自定义 getter 和 setter 进行中间操作。我也更喜欢将它与不可变类型一起使用,因为我觉得它最适合在整个程序中保持不变的值。

结论

在本文中,我们讨论了 Kotlin 的lateinit修饰符和惰性委托。我们展示了一些基本示例来展示它们的用途,并讨论了 Android 开发中的一些实际用例。

感谢您抽出宝贵时间阅读本入门指南!我希望您能够使用本指南在您的应用程序开发过程中实现这两个功能。

你可能感兴趣的:(kotlin,android,java)