Kotlin进阶之空安全、异常

Posted by AlexWan on 2017-07-17

空安全

可为空类型和非空类型

Koltin力求消除代码中空引用的问题(价值10亿的错误)。

多数编程语言(包括Java)中一个常见难题:访问空引用的成员导致的空引用异常。Java中称为NullPointerException异常,简称NPE

引起NPE异常的原因有:

  • 显性调用throw NullPointerException()
  • 使用!!操作符
  • 外部Java代码引起的异常
  • 未初始化的数据(如:未初始化的this)

Kolin对可以持有null和非空引用进行区分。如:String类型变量不能持有null引用:

var a: String = "abc"
a = null // 编译出错

需要使用String?:来声明可为空的变量

var b: String? = "abc"
b = null // OK

那么,可以调用a的方法或属性,并保证不会引起NPE异常:

val l = a.length

但是对于b,编译器则会报错

val l = b.length // 错误:变量b不能为空

为了能够使用b则需要进行一些调整。

null检查的条件语句

显式检查b是否为空

val l = if( b != null ) b.length else -1

编译器跟踪执行检查的信息,允许在if中调用length属性,也支持复杂的条件语句

if( b != null && b.length > 0){
print("String of length ${b.length}")
} else {
print("Empty string")
}

只在b为不可变时有效(如在检查和使用之间不变的局部变量;或有backing字段且不可复写的val成员),因为b可能会在非空检查后变为null

安全调用操作符

第二种可选择是使用?.安全调用操作符

b?.length

如果b不为空,返回b.length;否则返回null。表达式类型为Int?

安全调用在链式调用中会比较有用。如:假设有个Bob员工,对应一个部门(可能没有),部门有个主管,那么为了获取部门主管的名字,可以这样做:

bob?.department?.head?.name

如果上面任意属性为null,则结果都为null

如果只执行非空类型的操作,可用安全调用与let关键字共同使用。

cal listWithNulls: List<String?> = listOf("A" , null)
for(item in listWithNulls){
item?.let{ println(it)} // 输出"A"并忽略空值
}

Elvis操作符

若有个可空引用r,则”r非空时使用r,否则使用其他非空值”

val l: Int = if( b != null ) b.length else -1

上面完整的if表达式可使用?:Elvis操作符替代

val l: Int = b?.length ?: -1

?:表达式左边不为空时,则返回它的值,否则返回表达式左侧的值。只有在Elvis操作符左测为null时,才计算右侧表达式。

因为throwreturn在Kotlin中都是表达式,所以也可以在Elvis操作符右侧使用。因此方便用来检差函数参数。

fun foo(node: Node): String? {
val parent = node.getParent() ?: return null
val name = node.getName() ?: throw IllegalArgumentException("name expected")
// ...
}

!!操作符

NPE爱好者的第三种可选择操作符:b!!。如果b非空时,返回b的值;否则抛出NPE异常

val l = b!!.length

适用于需要显式判断的NPE时。

安全转换

如果对象非目标类型,常规转换会抛出ClassCastException类型转换异常,另外选项是如果转换未成功,使用安全转换返回null

val aInt: Int? = a as? Int

可为空类型集合

如果有一个组可为空类型集合且需要过滤非空元素,可以使用filterNotNull操作符

val nullableList: List<Int?> = listOf(1 , 2 , null , 4)
val intList: List<Int> = nullableList.filterNotNull()

异常

异常类

Kotlin中任意异常类都是Throwable子类。每个异常都具有异常信息堆栈信息可选原因

使用throw表达式抛出异常对象

throw MyException("Hi There!")

使用try表达式捕获异常

try {
// 代码
}
catch (e: SomeException) {
// 处理
}
finally {
// 可选 finally 块
}

catch块可以为0个或多个,finally块可忽略。但要至少要提供catchfinally其中一个。

Try是表达式

try为表达式(如:可以有返回值)

val a: Int? = try { parseInt(input) } catch (e: NumberFormatException ) { null }

try表达式返回值为try块或catch(出现异常时)块中最后一个表达式的值。finally块不影响表达式结果

检查型异常

Kotlin不支持检查型异常

如Java中StringBuilder类其中一个实现接口Appendable

Appendable append(CharSequence csq) throws IOException;

每次拼接String时,都要catch这些IOExceptions异常。所以使用时要手动捕获

try {
log.append(message)
}
catch (IOException e){
// Must be safe
}

Effective Java并不提倡这种方式,参考Item 65: Don’t ignore exceptions

Bruce Eckel说”Java需要检查型异常么?

Examination of small programs leads to the conclusion that requiring exception specifications could both enhance developer productivity and enhance code quality, but experience with large software projects suggests a different result – decreased productivity and little or no increase in code quality.

其他关于检查型异常的引用

Nothing类型

throw在Kotlin中是表达式,所以可以在Elvis表达式中使用

val s = person.name ?: throw IllegalArgumentException("Name required")

throw表达式返回类型为Nothing类型的特殊类型,这个类型没有值,经常用来标记无法到达的代码位置。可以使用Nothing类型无返回类型标记函数

fun fail(message: String): Nothing {
throw IllegalArgumentException(message)
}

当调用这个函数时,编译则会知道函数调用之后的代码不再执行:

val s = person.name ?: fail("Name required")
println(s) // // 's' is known to be initialized at this point

与Java交互

参考Java交互小节