共计 5053 个字符,预计需要花费 13 分钟才能阅读完成。
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 适配器的写法更加标准也更加举荐。