theme: fancy

highlight: a11y-dark

延迟初始化和密封类

本节的 Kotlin 课堂,我们就来学习延迟初始化和密封类这两部分内容。

对变量延长初始化

前面我们已经学习了 Kotlin 语言的许多特性,包括变量不可变,变量不可为空,等等。这些特性都是为了尽可能地保证程序安全而设计的,但是有些时候这些特性也会在编码时给我们带来不少的麻烦。

比如,如果你的类中存在很多全局变量实例,为了保证它们能够满足 Kotlin 的空指针检查语法标准,你不得不做许多的非空判断保护才行,即使你非常确定它们不会为空。

下面我们通过一个具体的例子来看一下吧,就使用刚刚的 UIBestPractice 项目来作为例子。如果你仔细观察 MainActivity 中的代码,会发现这里适配器的写法略微有点特殊:

class MainActivity : AppCompatActivity(), View.OnClickListener {

    private var adapter: MsgAdapter? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        adapter = MsgAdapter(msgList)
        ...
    }

    override fun onClick(v: View?) {
        ...
        adapter?.notifyItemInserted(msgList.size - 1)
        ...
    }
    
}

这里我们将 adapter 设置为了全局变量,但是它的初始化工作是在 onCreate() 方法中进行的,因此不得不先将 adapter 赋值为 null,同时把它的类型声明成 MsgAdapter?。

虽然我们会在 onCreate() 方法中对 adapter 进行初始化,同时能确保 onClick() 方法必然在 onCreate() 方法之后才会调用,但是我们在 onClick() 方法中调用 adapter 的任何方法时仍然要进行判空处理才行,否则编译肯定无法通过。

而当你的代码中有了越来越多的全局变量实例时,这个问题就会变得越来越明显,到时候你可能必须编写大量额外的判空处理代码,只是为了满足 Kotlin 编译器的要求。

幸运的是,这个问题其实是有解决办法的,而且非常简单,那就是对全局变量进行延迟初始化。

延迟初始化使用的是 lateinit 关键字,它可以告诉 Kotlin 编译器,我会在晚些时候对这个变量进行初始化,这样就不用在一开始的时候将它赋值为 null 了。

接下来我们就使用延迟初始化的方式对上述代码进行优化,如下所示:

class MainActivity : AppCompatActivity(), View.OnClickListener {

    private lateinit var adapter: MsgAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        adapter = MsgAdapter(msgList)
        ...
    }

    override fun onClick(v: View?) {
        ...
        adapter.notifyItemInserted(msgList.size - 1)
        ...
    }
    
}

可以看到,我们在 adapter 变量的前面加上了 lateinit 关键字,这样就不用在一开始的时候将它赋值为 null,同时类型声明也就可以改成 MsgAdapter了。由于 MsgAdapter 是不可为空的类型,所以我们在 onClick() 方法中也就不再需要进行判空处理,直接调用 adapter 的任何方法就可以了。

当然,使用 lateinit 关键字也不是没有任何风险,如果我们在 adapter 变量还没有初始化的情况下就直接使用它,那么程序就一定会崩溃,并且抛出一个 UninitializedPropertyAccessException 异常。

所以,当你对一个全局变量使用了 lateinit 关键字时,请一定要确保它在被任何地方调用之前已经完成了初始化工作,否则 Kotlin 将无法保证程序的安全性。

另外,我们还可以通过代码来判断一个全局变量是否已经完成了初始化,这样在某些时候能够有效地避免重复对某一个变量进行初始化操作,示例代码如下:

class MainActivity : AppCompatActivity(), View.OnClickListener {

    private lateinit var adapter: MsgAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        if (!::adapter.isInitialized) {
                adapter = MsgAdapter(msgList)
        }
        ...
    }
    
}

具体语法就是这样,::adapter.isInitialized 可用于判断 adapter 变量是否已经初始化。虽然语法看上去有点奇怪,但这是固定的写法。然后我们再对结果进行取反,如果还没有初始化,那么就立即对 adapter 变量进行初始化,否则什么都不用做。

以上就是关于延迟初始化的所有重要内容,剩下的就是在合理的地方使用它了,相信这对于你来说并不是什么难题。

使用密封类优化代码

由于密封类通常可以结合 RecyclerView 适配器中的 ViewHolder 一起使用,因此我们就正好借这个机会在本节学习一下它的用法。当然,密封类的使用场景远不止于此,它可以在很多时候帮助你写出更加规范和安全的代码,所以非常值得一学。

首先来了解一下密封类具体的作用,这里我们来看一个简单的例子。新建一个 Kotlin 文件,文件名就叫 Result.kt 好了,然后在这个文件中编写如下代码:

interface Result
class Success(val msg: String) : Result
class Failure(val error: Exception) : Result

这里定义了一个 Result 接口,用于表示某个操作的执行结果,接口中不用编写任何内容。然后定义了两个类去实现 Result 接口:一个 Success 类用于表示成功时的结果,一个 Failure 类用于表示失败时的结果,这样就把准备工作做好了。

接下来再定义一个 getResultMsg() 方法,用于获取最终执行结果的信息,代码如下所示:

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

getResultMsg() 方法中接收一个 Result 参数。我们通过 when 语句来判断:如果 Result 属于 Success,那么就返回成功的消息;如果 Result 属于 Failure,那么就返回错误信息。

到目前为止,代码都是没有问题的,但比较让人讨厌的是,接下来我们不得不再编写一个 else 条件,否则 Kotlin 编译器会认为这里缺少条件分支,代码将无法编译通过。但实际上 Result 的执行结果只可能是 Success 或者 Failure,这个 else 条件是永远走不到的,所以我们在这里直接抛出了一个异常,只是为了满足Kotlin编译器的语法检查而已。

另外,编写 else 条件还有一个潜在的风险。如果我们现在新增了一个 Unknown 类并实现 Result 接口,用于表示未知的执行结果,但是忘记在 getResultMsg() 方法中添加相应的条件分支,编译器在这种情况下是不会提醒我们的,而是会在运行的时候进入 else 条件里面,从而抛出异常并导致程序崩溃。

不过好消息是,Kotlin 的密封类可以很好地解决这个问题,下面我们就来学习一下。

密封类的关键字是 sealed class,它的用法同样非常简单,我们可以轻松地将 Result 接口改造成密封类的写法:

sealed class Result
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
}

可以看到,代码并没有什么太大的变化,只是将 interface 关键字改成了 sealed class。另外,由于密封类是一个可继承的类,因此在继承它的时候需要在后面加上一对括号,这一点我们在第 2 章就学习过了。

那么改成密封类之后有什么好处呢?你会发现现在 getResultMsg() 方法中的 else 条件已经不再需要了。

为什么这里去掉了 else 条件仍然能编译通过呢?这是因为当在 when 语句中传入一个密封类变量作为条件时,Kotlin 编译器会自动检查该密封类有哪些子类,并强制要求你将每一个子类所对应的条件全部处理。这样就可以保证,即使没有编写 else 条件,也不可能会出现漏写条件分支的情况。而如果我们现在新增一个 Unknown 类,并也让它继承自 Result,此时 getResultMsg() 方法就一定会报错,必须增加一个 Unknown 的条件分支才能让代码编译通过。

这就是密封类主要的作用和使用方法了。另外再多说一句,密封类及其所有子类只能定义在同一个文件的顶层位置,不能嵌套在其他类中,这是被密封类底层的实现机制所限制的。

了解了这么多关于密封类的知识,接下来我们看一下它该如何结合 MsgAdapter 中的 ViewHolder 一起使用,并顺便优化一下 MsgAdapter 中的代码。

观看 MsgAdapter 现在的代码,你会发现 onBindViewHolder() 方法中就存在一个没有实际作用的 else 条件,只是抛出了一个异常而已。对于这部分代码,我们就可以借助密封类的特性来进行优化。首先删除 MsgAdapter 中的 LeftViewHolde r和 RightViewHolder,然后新建一个 MsgViewHolder.kt 文件,在其中加入如下代码:

sealed class MsgViewHolder(view: View) : RecyclerView.ViewHolder(view)

class LeftViewHolder(view: View) : MsgViewHolder(view) {
    val leftMsg: TextView = view.findViewById(R.id.leftMsg)
}

class RightViewHolder(view: View) : MsgViewHolder(view) {
    val rightMsg: TextView = view.findViewById(R.id.rightMsg)
}

这里我们定义了一个密封类 MsgViewHolder,并让它继承自 RecyclerView.ViewHolder,然后让 LeftViewHolder 和 RightViewHolder 继承自 MsgViewHolder。这样就相当于密封类 MsgViewHolder只有两个已知子类,因此在 when 语句中只要处理这两种情况的条件分支即可。

现在修改 MsgAdapter 中的代码,如下所示:

class MsgAdapter(val msgList: List<Msg>) : RecyclerView.Adapter<MsgViewHolder>() {

    ...

    override fun onBindViewHolder(holder: MsgViewHolder, position: Int)     {
        val msg = msgList[position]
        when (holder) {
            is LeftViewHolder -> holder.leftMsg.text = msg.content
            is RightViewHolder -> holder.rightMsg.text = msg.content
        }
    }

    ...
    
}

这里我们将 RecyclerView.Adapter 的泛型指定成刚刚定义的密封类 MsgViewHolder,这样 onBindViewHolder() 方法传入的参数就变成了 MsgViewHolder。然后我们只要在 when 语句当中处理 LeftViewHolder 和 RightViewHolder 这两种情况就可以了,那个讨厌的 else 终于不再需要了,这种 RecyclerView 适配器的写法更加规范也更加推荐。


Maenj_Ba_lah
28 声望5 粉丝

真正的大师,永远怀着一颗学徒的心。