Kotlin:延迟初始化和密封类

Kotlin:延迟初始化和密封类

1. 对变量延迟初始化

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变量进行初始化,否则什么都不用做。

2 使用密封类优化代码

由于密封类通常可以结合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中的LeftViewHolderRightViewHolder,然后新建一个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,然后让LeftViewHolderRightViewHolder继承自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语句当中处理LeftViewHolderRightViewHolder这两种情况就可以了,这种RecyclerView适配器的写法更加规范也更加推荐。

你可能感兴趣的:(kotlin,android,java)