当前位置: 首页 > article >正文

Kotlin 知识点二 延迟初始化和密封类

对变量延迟初始化

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

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

下面我们通过一个具体的例子来看一下吧,就使用刚刚的UIBestP ractice 项目来作为例子。如
果你仔细观察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变量还没有初始化的
情况下就直接使用它,那么程序就一定会崩溃,并且抛出一个
UninitializedP ropertyA ccessEx ception 异常,如图

在这里插入图片描述
当对一个全局变量使用了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变量进行初始化,否则什么都不用做。

使用密封类优化代码

由于密封类通常可以结合RecyclerV iew 适配器中的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 当中存在,在Java 或
者是其他编程语言当中也普遍存在。

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

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

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

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

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

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

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

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

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

观看MsgAdapter现在的代码,你会发现onBindViewHolder()方法中就存在一个没有实际作
用的else条件,只是抛出了一个异常而已。对于这部分代码,我们就可以借助密封类的特性来
进行优化。首先删除MsgAdapter 中的Lef tViewHolder 和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终
于不再需要了,这种RecyclerV iew 适配器的写法更加规范也更加推荐。


http://www.kler.cn/a/562077.html

相关文章:

  • SGLang中context-length参数的默认值来源解析
  • 【MySQL】安装MySQL
  • 【学习笔记】Kubernetes
  • 【星云 Orbit-F4 开发板】03b. 按键玩法二:独立按键双击双击触发
  • IOS网络安全体系结构 网络安全体系架构
  • 【Java 常用注解学习笔记3】——Java 常用注解扩展与完善
  • MySQL 数据库基础详细解释和示例
  • 数据结构之【链表简介】
  • vi 编辑器的使用
  • DeepSeek开源周Day2:DeepEP - 专为 MoE 模型设计的超高效 GPU 通信库
  • Web前端开发——HTML基础
  • Cassini_Network-Aware Job Schedulingin Machine Learning Clusters
  • 【【Systemverilog学习参考 简单的加法器验证-含覆盖率】】
  • unity学习51:所有UI的父物体:canvas画布
  • 鸿蒙5.0实战案例:har和hsp的转换
  • “深入解析 SQL Server 子查询:从基础到应用”
  • 安全开发-环境选择
  • AGI分级探索:从OpenAI到DeepMind,展望未来AI图景
  • Ubuntu从零创建Hadoop集群
  • idea里的插件spring boot helper 如何使用,有哪些强大的功能,该如何去习惯性的运用这些功能