(译)Kotlin中的Lateinits vs Nullables

Kotlin给了我们很多简单明了的方式处理可空的变量,从而减少出问题的风险。当然前提是你正确地使用它。

Lateinit修饰符

通常来说,kotlin中所有不可空的属性都必须被正确地初始化。你可以用很多方式实现:

  • 在主构造器中,
  • 在初始化代码块中,
  • 直接在类里的属性声明中,
  • 在getter方法中*,
  • 用delegate实现*.

*的方法严格意义上来说并不是初始化,但是它使得编译器理解这些变量是非null

但如果一个属性是生命周期驱动的(例如:一个button的引用,它会在Activity的生命周期中被inflated出来)或者它需要通过注入来初始化,那就没办法提供一个non-null的初始化,就只能把它声明成可空的。这就需要你每次使用它都进行null checks,很麻烦,尤其是在你百分百确定它在被使用之前,一定会被初始化的时候。

所以kotlin给这种情况提供了一种简单的解决方案,lateinit修饰符。这样就不需要每次都做null checks了,当然如果用到的时候该属性没有被初始化,系统就会抛出UninitializedPropertyAccessException

Lateinits vs nullables

尽管Lateinits本身是kotlin提供的非常有用的feature,但它更有可能会在很多不那么确定会被初始化(例如:有条件的初始化或者仅仅是初始化比较晚)的情况下滥用,从而造成空安全的风险,让代码变得更像java。下面是我的一些主观的经验。

Lateinit 的第一种使用情况:生命周期的开始就初始化

当一个属性可以在使用它的某个类的生命周期开始的时候,比如Activity.onCreate(),就初始化的情况下,推荐使用Lateinits。 比较常见的情况是项目中使用了依赖注入的框架,比如Dagger.

abstract class BaseActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
}
}

class LoginActivity : BaseActivity() {

@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory

lateinit var viewModel: LoginViewModel

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

viewModel = ViewModelProviders.of(this, viewModelFactory).get(LoginViewModel::class.java)

setContentView(R.layout.login_activity)
}
}

或者当你希望getSystemService()的时候:

class MainActivity : AppCompatActivity() {

lateinit var alarmManager: AlarmManager

// This wouldn't work:
// val alarmService = getSystemService(AlarmManager::class.java)

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
alarmManager = getSystemService(AlarmManager::class.java)
}

不然的话,就会报IllegalStateException的错误,说system services are not available to Activities before onCreate()

同时这样也可以避免在Object构建的时候使用this,导致线程安全的问题

class MyActivity : AppCompatActivity() {

lateinit var alien: Alien
// instead of:
// val alien = Alien(this)

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
alien = Alien(this)
}
}

Lateinit 的第二种使用情况: fail-fast approach

在开发的过程中,我们会希望尽可能地暴露程序中的问题,并且当出现问题的时候直接crash而不是被catch然后带着缺陷继续运行。这样我们就能尽可能地修复在开发过程当中暴露的问题。如果代码逻辑比较简明的话,这是一种很有用的方法。

比如,一个Activity需要使用MediaPlayer来播放音乐,代码如下:

class PlayerActivity : AppCompatActivity() {

lateinit var mediaPlayer: MediaPlayer
lateinit var mediaUri: Uri

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_player)
mediaUri = intent.getParcelableExtra("mediaUri")
}

override fun onStart() {
super.onStart()
mediaPlayer = MediaPlayer()
mediaPlayer.setDataSource(this, mediaUri)
mediaPlayer.prepare()
mediaPlayer.start()
}

override fun onStop() {
super.onStop()
mediaPlayer.stop()
mediaPlayer.release()
}
}

上一段代码有一个很明显的风险点在mediaUri 是在onCreate中通过传进来的intent解析出来的,如果intent中没传mediaUri,那就抛出java.lang.IllegalStateException: intent.getParcelableExtra("mediaUri") must not be null

这段代码简单清晰,所以不难发现问题。但如果到了一个大的工程里就麻烦了。

image.png

Nullable 的第一种使用情况:完全不希望出crash的情况下

有些情况下(译者:最好是所有情况下!)你希望全力避免app出crash,即使程序运行出了问题。 比如上面的音乐播放的例子,如果我们改成nullable的形式:

class PlayerActivity : AppCompatActivity() {

var mediaPlayer: MediaPlayer? = null
var mediaUri: Uri? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_player)
mediaUri = intent.getParcelableExtra("mediaUri")
}

override fun onStart() {
super.onStart()

mediaUri?.let {
val player = MediaPlayer()
player.setDataSource(this, it)
player.prepare()
player.start()
mediaPlayer = player
}
}

override fun onStop() {
super.onStop()
mediaPlayer?.stop()
mediaPlayer?.release()
}
}

现在,如果没有在intent中拿到mediaUri,那app只是不会播放音乐了而不是直接crash,不过用户会很惊讶为啥没声儿(他可能会认为他的手机坏了)。这是否比crash更好(译者:当然!)取决于开发者的判断。但是这种方式的好处是,现在我们有机会在属性为null的时候做些什么处理这种未初始化的情况。

注:从Kotlin1.2开始你可以对lateinit的属性用.isInitialized ,当然这也有限制( 文档 ):

This check is only available for the properties that are lexically accessible, i.e. declared in the same type or in one of the outer types, or at top level in the same file.

Nullable 的第二种使用情况:初始化的时机很晚

下面的代码,Activity中用lateinit的变量保存了一个ItemData,然后再点击事件中调用startActivityForResult(),最后在onActivityResult()里使用了这个lateinit的变量。

class SomeActivity : AppCompatActivity() {

lateinit var selectedItemData: ItemData

// 很多代码 ...

private fun doSomethingWithItem(data: ItemData) {
// 很多代码 ...

if (...) { // 复杂冗长的判断
if (...) {
data?.anotherData?.let { // 也许不会继续进行而且不会通知上层
selectedItemData = data
startActivityForResult(...)
}
}
}
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
doSomethingElse(selectedItemData)
}
}

通常来说,我会避免这种“初始化太晚”的代码结构。这种情况下推荐使用nullable,因为大部分时候变量都是null

怎么更好地使用“?”

如果需要保证空安全就使用 把属性定为可空。lateinit如果用的好的话也是有它的优势在的。 但千万别把它变成了NullPointerException的标志。

译者注:

个人的建议是,除了dagger注入的情况之外一概不用lateinit。因为虽然lateinit的使用会带来代码上面的一些便利,但是比起所引入的crash风险来说,这真的值得么?这就好像用!!的方式来保证属性的非空一样,虽然kotlin确实提供了这样的方式,但是无论如何还是太危险了。

0 个评论

要回复文章请先登录注册