Kotlin 基础03 - 空指针检查机制

空指针检查

Kotlin 默认所有的参数和变量都不可为空。

1
2
3
4
5
6
7
8
9
fun main() {
// error! Null can not be a value of a non-null type Study
doStudy(null)
}

fun doStudy(study: Study) {
study.readBooks()
study.doHomework()
}

Kotlin 将空指针异常的检查提前到了编译期,如果程序存在空指针异常的风险,那么在编译的时候会直接报错。

可空类型

Kotlin 提供了另外一套可为空的类型系统,在使用时需要在编译期就将所有潜在的空指针异常都处理掉,否则代码无法编译通过。

使用上就是在类名的后面加上一个问号。比如,Int? 表示可为空的整形,String? 表示可为空的字符串。

如果使用了 ? 而不处理潜在可能的空指针异常,不能通过编译:

1
2
3
4
5
6
7
8
9
10
fun main() {
doStudy(null)
}

fun doStudy(study: Study?) {
// Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type Study?
study.readBooks()
// Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type Study?
study.doHomework()
}

处理掉空指针异常,可以通过编译:

1
2
3
4
5
6
fun doStudy(study: Study?) {
if (study != null) {
study.readBooks()
study.doHomework()
}
}

判空辅助工具

?. 操作符,当对象不为空时正常调用相应的方法,为空时什么也不做

对于如下代码:

1
2
3
4
if (study != null) {
study.readBooks()
study.doHomework()
}

可以转换为:

1
2
study?.readBooks()
study?.doHomework()

?: 操作符,操作符左右都接收一个表达式,如果左边表达式的结果不为空就返回左边表达式的结果,否则就返回右边表达式的结果

1
2
3
4
5
val c = if (a != null) {
a
} else {
b
}

可以简化为:

1
val c = a ?: b

一个同时使用 ?.?: 的例子,对如下获得文本长度的函数:

1
2
3
4
5
6
fun getTextLength(text: String?): Int {
if (text != null) {
return text.length
}
return 0
}

可以简化为:

1
fun getTextLength(text: String?) = text?.length ?: 0

当 text 为空时,text?.length 会返回一个 null,再借助 ?: 让它返回0。


!! 非空断言操作符。

有时可能从逻辑上已经将空指针异常处理了,但是 Kotlin 的编译器并不能认知到,这个时候还是会编译失败,常见对于全局变量进行判断时。

1
2
3
4
5
6
7
8
9
10
11
12
13
var content: String? = null

fun main() {
if (content != null) {
printUpperCase()
}
}

fun printUpperCase() {
// error! Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?
val toUpperCase = content.toUpperCase()
println(toUpperCase)
}

printUpperCase() 函数并不知道外部已经对 content 变量进行了非空检查,在调用 toUpperCase() 方法时,还认为这里存在空指针风险,从而无法编译通过。

如果想要强行通过编译,可以使用非空断言工具,写法是在对象的后面加上 !!

1
2
3
4
fun printUpperCase() {
val toUpperCase = content!!.toUpperCase()
println(toUpperCase)
}

这是一种有风险的写法,意在告诉 Kotlin 不需要做空指针检查,如果出现问题,可以直接抛出空指针异常。

当想使用非空断言工具时,最好想想是不是有更好的实现方式。你最自信这个对象不会为空的时候,其实可能就是一个潜在空指针异常发生的时候。


辅助工具 let 不是操作符或关键字,而是一个函数。这个函数提供函数式 API 的编程接口,并将原始调用对象作为参数传递到 Lambda 表达式中,其中代码会立即执行。

上面使用 ?. 的代码:

1
2
study?.readBooks()
study?.doHomework()

实际对应的代码就是:

1
2
3
4
5
6
if (study != null) {
study.readBooks()
}
if (study != null) {
study.doHomework()
}

每一次使用 ?. 都进行了一次 if 判断。

使用 let 结合 ?. 可以进行简化

1
2
3
4
5
6
fun doStudy(study: Study?) {
study?.let { stu -> {
stu.readBooks()
stu.doHomework()
} }
}

又因为当 Lambda 表达式的参数列表只有一个参数时,可以使用 it 关键字代替

1
2
3
4
5
6
fun doStudy(study: Study?) {
study?.let {
it.readBooks()
it.doHomework()
}
}

这里 let 的使用有点类似于 Java 8 中的 Optional 。

let 函数是可以处理全局变量的判空问题的,if 无法做到这一点。

1
2
3
4
5
6
7
8
9
10
var study: Study? = null

fun doStudy() {
if (study != null) {
// error! Smart cast to 'Study' is impossible, because 'study' is a mutable property that could have been changed by this time
study.readBooks()
// error!
study.doHomework()
}
}

因为全局变量的值随时都有可能被其他线程所修改,即时做了判空处理,仍然无法保证 if 语句中的 study 变量没有空指针风险。

使用 let 编译通过:

1
2
3
4
5
6
7
8
var study: Study? = null

fun doStudy() {
study?.let {
it.readBooks()
it.doHomework()
}
}