Kotlin
语言的许多特性,包括变量不可变,变量不可为空,等等。这些特性都是为了尽可能地保证程序安全而设计的,但是有些时候这些特性也会在编码时给我们带来不少的麻烦。
比如,如果类中存在很多全局变量实例,为了保证它们能够满足Kotlin
的空指针检查语法标准,不得不做许多的非空判断保护才行,即使确定它们不会为空。例如以下代码:
class MainActivity : BaseActivity(), View.OnClickListener {
private var adapter: MsgAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
...
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 : BaseActivity(), View.OnClickListener {
private lateinit var adapter: MsgAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
...
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 : BaseActivity(), View.OnClickListener {
private lateinit var adapter: MsgAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
...
if (!::adapter.isInitialized) {
adapter = MsgAdapter(msgList)
}
...
}
override fun onClick(v: View?) {
...
adapter.notifyItemInserted(msgList.size - 1)
...
}
}
具体语法就是这样,::adapter.isInitialized
可用于判断adapter
变量是否已经初始化。 虽然语法看上去有点奇怪,但这是固定的写法。然后我们再对结果进行取反,如果还没有初始化,那么就立即对adapter
变量进行初始化,否则什么都不用做。
由于密封类通常可以结合RecyclerView
适配器中的ViewHolder
一起使用。这里来看一个简单的例子,新建一个Kotlin
文件Result.kt
,然后在这个文件中编写如下代码:
interface Result
class Success(val msg: String) : Result
class Failure(val error: String) : 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()
sealed [siːld] 密封的;未知的
代码并没有什么太大的变化,只是将interface
关键字改成了sealed class
。另外,由于密封类是一个可继承的类,因此在继承它的时候需要在后面加上一对括号。
那么改成密封类之后的好处是getResultMsg()
方法中的else
条件已经不再需要了,如下所示:
fun getResultMsg(result: Result) = when (result) {
is Success -> result.msg
is Failure -> "Error is ${result.error.message}"
}
这是因为当在when
语句中传入一个密封类变量作为条件时,Kotlin
编译器会自动检查该密封类有哪些子类,并强制要求将每一个子类所对应的条件全部处理。 这样就可以保证,即使没有编写else
条件,也不可能会出现漏写条件分支的情况。而如果现在新增一个Unknown
类,并也让它继承自Result
,此时getResultMsg()
方法就一定会报错,必须增加一个Unknown
的条件分支才能让代码编译通 过。
另外再多说一句,密封类及其所有子类只能定义在同一个文件的顶层位置,不能嵌套在其他类中,这是被密封类底层的实现机制所限制的。
接下来看一下它该如何结合RecyclerView.Adapter
中的ViewHolder
一起使用。
class MsgAdapter(val msgList: List<Msg>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
inner class LeftViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val leftMsg: TextView = view.findViewById(R.id.leftMsg)
}
inner class RightViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val rightMsg: TextView = view.findViewById(R.id.rightMsg)
}
override fun getItemViewType(position: Int): Int {
val msg = msgList[position]
return msg.type
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
if (viewType == Msg.TYPE_RECEIVED) {
val view =
LayoutInflater.from(parent.context).inflate(R.layout.msg_left_item, parent, false)
LeftViewHolder(view)
} else {
val view =
LayoutInflater.from(parent.context).inflate(R.layout.msg_right_item, parent, false)
RightViewHolder(view)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val msg = msgList[position]
when (holder) {
is LeftViewHolder -> holder.leftMsg.text = msg.content
is RightViewHolder -> holder.rightMsg.text = msg.content
else -> throw IllegalArgumentException()
}
}
override fun getItemCount() = msgList.size
}
在onBindViewHolder()
方法中就存在一个没有实际用的else
条件,只是抛出了一个异常而已。对于这部分代码,就可以借助密封类的特性来进行优化。首先删除MsgAdapter
中的LeftViewHolder
和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
这两种情况就可以了,这种RecyclerView
适配器的写法更加规范也更加推荐。