Kotlin 的延迟初始化和密封类

对变量进行延迟初始化

在 Kotlin 中,变量不可为空的特性增加了程序的空安全性,但有时也会带来一些麻烦。比如,当一个类中的某些成员变量需要在该类实例化之后才能被初始化时,我们只好将这些成员变量声明为可空类型。

并且在后续使用该变量时,你必须添加上非空判断保护才行(如 ?. 空安全调用或 !!. 非空断言),即使你能确定该变量在使用前已被初始化,在使用时不为空。

以之前的 UIBestPractice 项目为例,我们来到 MainActivity 中,你会发现对 binding 的访问密密麻麻的都是 ?.

kotlin 复制代码
class MainActivity : AppCompatActivity() {

    ...
    private var binding: ActivityMainBinding? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 初始化视图绑定对象
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding?.root)

        initMsg() 

        binding?.toolbar?.let { nonNullToolbar ->
            setSupportActionBar(nonNullToolbar)
            supportActionBar?.title = "UIBestPractice"
        }

        val layoutManager = LinearLayoutManager(this)
        binding?.recyclerView?.layoutManager = layoutManager

        adapter = MsgAdapter(msgList)
        binding?.recyclerView?.adapter = adapter

        binding?.send?.setOnClickListener {
            val content = binding?.inputText?.text.toString()

            if (content.isNotEmpty()) {
                val msg = Msg(content, Msg.TYPE_SENT)
                msgList.add(msg)
                adapter.notifyItemInserted(msgList.size - 1) 
                binding?.recyclerView?.scrollToPosition(msgList.size - 1) 
                binding?.inputText?.setText("")
            }
        }
    }

    ...
}

可以看到 binding 是一个成员变量,它的初始化工作发生在 onCreate() 方法中,不发生在 MainActivity 构造时(构造函数或 init 块中)。所以我们不得不将它的类型声明为 ActivityMainBinding?,并且同时赋值为 null

这样做之后,那后续访问 binding 时,binding 必定不为空。但由于其类型是可为空的,所以我们必须进行判空处理,否则无法通过编译,这就使得代码中充满了 ?.

当类中加入了更多这类的成员变量实例,这个问题会愈发的明显。但好在我们是有解决方法的,就是利用 Kotlin 提供的延迟初始化

延迟初始化使用 lateinit 关键字,它告诉编译器:我们等会对这个非空类型的变量进行初始化,并且是使用之前进行初始化,所以你也不用提示我,声明该变量时没有被初始化了。

我们来对当前的代码进行优化,结果如下所示:

kotlin 复制代码
class MainActivity : AppCompatActivity() {

    ...
    private lateinit var binding: ActivityMainBinding // 使用 lateinit 声明

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        initMsg() 

        binding.toolbar.let { nonNullToolbar ->
            setSupportActionBar(nonNullToolbar)
            supportActionBar?.title = "UIBestPractice"
        }

        val layoutManager = LinearLayoutManager(this)
        binding.recyclerView.layoutManager = layoutManager

        adapter = MsgAdapter(msgList)
        binding.recyclerView.adapter = adapter

        binding.send.setOnClickListener {
            val content = binding.inputText.text.toString()

            if (content.isNotEmpty()) { 
                val msg = Msg(content, Msg.TYPE_SENT)
                msgList.add(msg)
                adapter.notifyItemInserted(msgList.size - 1) 
                binding.recyclerView.scrollToPosition(msgList.size - 1) 
                binding.inputText.setText("") 
            }
        }
    }

    ...
}

我们在声明 binding 变量时,使用了 lateinit var 来声明。这样,我们就可以将其类型声明为非空的 ActivityMainBinding 了,并且不用在一开始就给它赋值为 null。这样在后续使用 binding 变量时,可以直接通过 . 来访问它,而无需使用 ?. 操作符进行判空处理,代码是不是看起来清爽多了?

但如果你在 binding 初始化之前就访问了它,程序在运行时会抛出异常:kotlin.UninitializedPropertyAccessException: lateinit property binding has not been initialized

所以当你声明了一个延迟初始化的成员变量时,一定要确保在使用该全局变量之前,它就已经被初始化了。

另外,你可以通过 ::propertyName.isInitialized 来判断某个延迟初始化变量是否已经被初始化了,可以避免重复的初始化工作。虽然我们这里不需要这个操作,但还是看看:

kotlin 复制代码
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    if (!::binding.isInitialized) {
        binding = ActivityMainBinding.inflate(layoutInflater)
    }

    setContentView(binding.root)

    ...
}

使用密封类优化代码

密封类是 Kotlin 中一个特性,它用于表示受限制的类继承结构。当你希望一个类的所有直接子类可知,并且不希望在别的地方有新的子类继承这个类,你就可以使用密封类。 它常常可以和 RecyclerView 适配器中的 ViewHolder 或者表示状态的类(如网络请求结果)一起使用,以提高代码的类型安全性。

我们来看看它的作用。假设我们需要表示某个操作的结果,可以创建一个 Result.kt 文件,代码如下:

kotlin 复制代码
interface Result
class Success(val msg: String) : Result
class Failure(val error: Exception) : Result

上述代码中,我们首先定义了一个 Result 接口,用于表示某次操作的结果。然后定义两个类去实现它,一个表示成功的结果,另一个表示失败的结果。

我们再来定义一个 getResultMsg() 方法,用于获取结果的信息,代码如下:

kotlin 复制代码
fun getResultMsg(result: Result) = when (result) {
    is Success -> result.msg
    is Failure -> result.error.message
    else -> throw IllegalArgumentException("Unknown Result type encountered")
}

方法会接收一个 Result 对象,然后通过 when 语句判断它的具体类型,并返回对应的消息。但我们不得不加上 else 语句,否则会因为缺少条件分支,导致编译不通过,因为编译器不知道 when 语句是否覆盖了所有可能的情况。而实际上,这个 else 条件是永远无法满足的,所以我们在这个抛了一个异常,只是为了满足语法规则。

更麻烦的是,如果我们新增了一个 Result 接口的实现类,比如 Unknown(表示未知结果),而忘了去 getResultMsg() 方法的 when 语句中对新类型添加条件判断分支。这样会导致当我们传入了 Unknown 对象时,就会来到 else 分支,抛出异常导致程序崩溃。这种错误在编译时是无法被发现的。

不过,Kotlin 的密封类可以解决以上问题。

密封类的关键字是 sealed class。使用的话,很简单,以前面的 Result 接口为例:

kotlin 复制代码
sealed class Result  // 声明为 sealed class
class Success(val msg: String) : Result()
class Failure(val error: Exception) : Result()

fun getResultMsg(result: Result) = when (result) {
    is Success -> result.msg
    is Failure -> result.error.message
}

可以看到 getResultMsg 方法的 when 语句中,不再需要 else 条件分支了。

那为什么去掉 else 条件分支也能编译通过呢? 因为当 when 语句的判断对象是一个密封类对象时,Kotlin 编译器会自动检查该密封类的所有直接子类,并强制 要求你将每一个已知的直接子类都作为一个分支条件进行处理,这样就可以保证:即使没有 else 条件判断,也不会漏写分支条件。

现在,我们再新增一个 Unknown 类,继承自 Result 密封类,你会发现 when 语句报错了,报错信息为:'when' expression must be exhaustive, add necessary 'is Unknown' branch or 'else' branch instead,这个编译时错误正是密封类的强大之处,让我们不会遗漏掉已知情况。

kotlin 复制代码
sealed class Result
class Success(val msg: String) : Result()
class Failure(val error: Exception) : Result()
class Unknown(val info: String? = null) : Result()

所以我们为了程序能够运行,只能在 when 语句中添加上处理 Unknown 类型的条件分支:

kotlin 复制代码
fun getResultMsg(result: Result) = when (result) {
    is Success -> result.msg
    is Failure -> "Error is ${result.error.message}"
    is Unknown -> "Unknown Result,${result.info}"
}

另外,密封类及其所有子类只能定义在同一个包和同一个模块,这是被密封类底层的实现机制所限制的,这样才能让 Kotlin 编译器知道一个密封类的所有直接子类。

了解完密封类的作用后,我们来看看它是如何与 RecyclerView 适配器中的 ViewHolder 一起使用的,以优化多视图类型的处理。

假设我们的 MsgAdapter 需要展示两种不同的消息,分别是收到的消息和发送的消息。我们会定义两个不同的 ViewHolder

来到 MsgAdapter 中,代码如下:

kotlin 复制代码
class MsgAdapter(private val msgList: List<Msg>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    inner class LeftViewHolder(private val msgLeftItemBinding: MsgLeftItemBinding) :
        RecyclerView.ViewHolder(msgLeftItemBinding.root) {
        fun bind(msg: Msg) {
            msgLeftItemBinding.leftMsg.text = msg.content
        }
    }

    inner class RightViewHolder(private val msgRightItemBinding: MsgRightItemBinding) :
        RecyclerView.ViewHolder(msgRightItemBinding.root) {
        fun bind(msg: Msg) {
            msgRightItemBinding.rightMsg.text = msg.content
        }
    }
 
    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val msg = msgList[position]

        when (holder) {
            is LeftViewHolder -> holder.bind(msg)
            is RightViewHolder -> holder.bind(msg)
            else -> throw IllegalArgumentException()
        }
    }
    
    ...
}

可以看到,onBindViewHolder 方法的 when 语句中的 else 条件分支就是没必要的,只是抛出了一个异常,现在我们借助密封类来改造一下。

新建一个 MsgViewHolder.kt 文件,代码如下:

kotlin 复制代码
sealed class MsgViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)

class LeftViewHolder(private val msgLeftItemBinding: MsgLeftItemBinding) :
    MsgViewHolder(msgLeftItemBinding.root) {
    fun bind(msg: Msg) {
        msgLeftItemBinding.leftMsg.text = msg.content
    }
}

class RightViewHolder(private val msgRightItemBinding: MsgRightItemBinding) :
    MsgViewHolder(msgRightItemBinding.root) {
    fun bind(msg: Msg) {
        msgRightItemBinding.rightMsg.text = msg.content
    }
}

我们定义了一个密封类 MsgViewHolder,让它继承自 RecyclerView.ViewHolder,然后让 LeftViewHolderRightViewHolder 继承自 MsgViewHolder

然后再来修改 MsgAdapter 适配器,代码如下:

kotlin 复制代码
class MsgAdapter(private val msgList: List<Msg>) : RecyclerView.Adapter<MsgViewHolder>() {
    
    ...
        
    override fun onBindViewHolder(holder: MsgViewHolder, position: Int) {
        val msg = msgList[position]

        when (holder) {
            is LeftViewHolder -> holder.bind(msg)
            is RightViewHolder -> holder.bind(msg)
        }
    }

    ...
}

我们将 RecyclerView.Adapter 的泛型指定为了刚刚定义的 MsgViewHolder 密封类,这样,在重写 onBindViewHolder 方法时,其 holder 参数的类型就可以变为 MsgViewHolder,然后因为 MsgViewHolder 是一个密封类,所以我们就可以把 else 分支删除了。并且之后新增了 MsgViewHolder 的子类,编译器会提醒我们 when 语句少了对新的子类的条件处理,从而避免了运行时错误。

相关推荐
移动开发者1号1 小时前
Jetpack Compose瀑布流实现方案
android·kotlin
移动开发者1号1 小时前
Android LinearLayout、FrameLayout、RelativeLayout、ConstraintLayout大混战
android·kotlin
移动开发者1号1 小时前
ListView与RecyclerView区别总结
android·kotlin
移动开发者1号1 小时前
OkHttp 3.0源码解析:从设计理念到核心实现
android·kotlin
casual_clover17 小时前
Android 之 kotlin语言学习笔记三(Kotlin-Java 互操作)
android·java·kotlin
梓仁沐白17 小时前
【Kotlin】数字&字符串&数组&集合
android·开发语言·kotlin
Dola_Pan1 天前
Android四大组件通讯指南:Kotlin版组件茶话会
android·开发语言·kotlin
移动开发者1号1 天前
应用启动性能优化与黑白屏处理方案
android·kotlin
移动开发者1号1 天前
Android处理大图防OOM
android·kotlin