Kotlin中很多语法特性,如变量不可变,变量不可为空,等等 这些特性都是为了尽可能地保证程序安全而设计的,比如你的类中存在很多全局变量实例,为了保证它们的能够满足Kotlin的空指针检查语句标准,你不得不做非空判断保护,即使你非常确定它们不会为空。
下面距离看一下 :
class MainActivity : AppCompatActivity() {
private var s: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
s = "test"
Log.d("TAG", "onCreate: ${s!!.length}")
}
}
我们将s 设置为了全局变量 , 但是它的赋值工作在onCreate()方法进行的,因此不得不将s赋值为null。
虽然你确定在打印前已经将s赋值成功,但是打印s的长度仍然要进行判空处理才行,否则编译不通过。
当你的代码中越来越多的全局变量实例时,这个问题就会变得越来越明显,到时候可能必须写大量额外判空处理的代码,却只是为了满足Kotlin的编译要求。
幸运的是,这个问题有解决办法的且非常之简单,就是对全局变量进行延迟初始化。
初始化使用的关键字 lateinit ,它可以高速编译器,我会在晚些时候对这个变量进行初始化,这样就不用一开始就赋值为null了。
class MainActivity : AppCompatActivity() {
private lateinit var s: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
s = "test"
Log.d("TAG", "onCreate: ${s.length}")
}
}
可以看到,加上了lateinit关键字 ,这样一开始就不用赋值为null了,打印长度的时候也不用进行判空处理了,当然使用lateinit关键字 也不是没有风险,如果没有进行赋值,那么程序一定会崩溃,并抛出异常。
另外,我们可以通过代码来判断全局变量是否已完成了初始化工作,这样某些时候可以有效避免重复对某一个变量进行初始化操作。
class MainActivity : AppCompatActivity() {
private lateinit var s: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (!::s.isInitialized) {
s = "test"
}
Log.d("TAG", "onCreate: ${s.length}")
}
}
新建一个Kotlin文件,代码如下 :
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编译不通过,其实代码永远也走不到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。密封类是一个可继承的类,因此在继承它的时候还需要加上一对括号。
密封类的优点是,可以再getResultMsg() 方法中取消else条件语句。
为什么取消掉else条件语句还能编译通过呢,Kotlin编译器会自动检查该密封类有哪些子类,并强制要求将每一个子类所对应的条件全部处理。这样就可以保证,即使没有else条件,也不可能出现漏写的情况,如果现在新增一个Unknown类,并也让它继承自Result,此时getResultMsg()方法就一定会报错,必须添加Unknown语句条件才能编译通过。
注意:密封类及其所有子类只能定义同一个文件的顶层位置,不能嵌套在其他类中,这也是被密封类底层实现机制所限制的。