Kotlin空安全怎样做的(基础)

144 阅读8分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第2天,点击查看活动详情

Kotlin空安全怎样做的(基础)

Kotlin is a modern but already mature programming language aimed to make developers happier. It's concise, safe, interoperable with Java and other languages, and provides many ways to reuse code between multiple platforms for productive programming.

Kotlin官网可以找到这样一句话,这是对于这门编程语言的介绍——旨在让开发者更加快乐,想必是解决一些长久以来令开发者头疼的问题。随后,与Java等语言进行比较,突出它的简洁、安全、互操作性、重用性、跨平台上的优势,这么听起来的确是挺快乐的

仔细想想看,使用Java时,常常陪伴的头疼问题,一定会有空指针一席之地,写的时候稍不留神,可能运行起来就不知道什么时候直接崩了,而Kotlin所提倡的安全真好与此紧密相关

1. 空引用

在Java编程语言中,有一个null的关键字(其他语言可能也有类似的概念),代表的含义是不分配内存,没有引用
Java中的原生类型像intboolean等都会拥有默认值,null其实代表的就是引用类型的默认值

image.png

似乎从这几点上来看,null代表了一种状态,对于程序来讲,有着特殊的意义(重要的描述作用),然而正是这份特殊,让编写程序的开发者时常牵肠挂肚,毕竟对于其的大多数直接使用可能造成异常
常常出现的场景比如像变量未实例化有效值就调用,或者主动赋值null(如释放资源或添加判断某种类型的flag等),尤其是当程序变得庞大后,这样问题可能会藏得更深,平时可能难以发现,即使进行代码走查之类的审查,也难以面面俱到,而Kotlin在此将它对于该类问题的解决方案放入了语言特色之中

2. Kotlin的空安全

首先,Kotlin在变量的类型方面进行了创新,区分了可空类型不可空类型

2.1. 可空类型和不可空类型

空指的就是null,字面上的理解就是一种类型可以为null,另一种不可以
需要注意的一点是Kotlin的数据类型并不像Java一样划分基本类型和引用类型,而只有引用类型,但是会在编译时进行优化

不可空的类型的变量类型就和Java的普通引用类型一样,例如:String,但是不能给其赋null,因为不可空,因此需要始终需要保证该类型的对象有值

val s: String = ""

或者

val s = ""

后者Kotlin编译器根据字面量可以推断出该变量的类型非空(当然,线索要充分)

image.png

而如果在初始化的时候,赋值null,这时就检测出问题了

image.png

这里显式声明的变量为非空,然后给了空,这不是它所能承受的,在编译的时候就发现并报错了
如果想要让一个变量能够接受空值null,就必须使用可空类型

可空类型的定义与不可空类型只有一个?的差别,因为不确定是不是为null,但是有这种可能,所以加个?表示可疑(我猜的)

val s: String? = null  // 可空的String

可空的类型可以使用空值或对应类型的非空值初始化,但是不可以在声明时直接用字面值推导,因为线索不够,大家都是引用类型,根据null区分不了,非空值会被默认为不可空类型,只能显式声明类型

image.png

不可空类型就好比免检产品,只要确认你是,就不管了,直接放行;而可空类型代表的就是外来的可疑分子,不可信任,必须“特殊照顾”,才能确保安全,那么,怎么“照顾”呢?

2.2. 可空类型的检测

智能类型转换

可空类型的值无非可空与不可空,因此如果条件语句判断不为空,编译器将自动转换其类型为不可空

val s: String? = "abc"
if (s != null) {  // 判断完自动转换为String
    println(s.length)
}

image.png

在判断完后,该分支上的非空变量发生了智能类型转换

安全访问

对于可空类型变量,不能直接用.调用方法和访问成员变量,而需要使用安全调用操作符?.

val s: String? = "abc"
println(s?.length)

像这样,只有调用该可空类型处于非空的状态下,后面的内容才会被成功调用,否则直接返回null

println(
    if (s != null) {
        s.length   // 不为空调用
    } else {
        null    // 为空直接null,下面不执行
    }
)

这段代码和?.的作用是等效的,因此使用?.得到的内容依然会是一个可空类型

val s: String? = "abc"
val length = s?.length   // 依然可空
println(length)

image.png

Elvis操作符

有些时候,其实我们并不想让安全调用返回可空类型,因为实际场景中可能会有默认值,所以我们认为它不应该为空,或者说应该有个默认值兜底,这个时候就有了Elvis操作符

val s: String? = "abc"
val length = s?.length ?: 0   // 0作为默认值兜底,为非空类型
println(length)

?:就是Elvis操作符,如果你使用过Java中的三元操作符一定会觉得它们有点像,?:会判断左边的内容,如果为null会去取右侧的值;如果非null,就直接取左边的值,这样就保证无论如何都不为null,当然,别右边写null

另外,操作符右边也可以是表达式,比如returnthrow直接终止不满足条件的执行或者抛出特定的异常

val length = s?.length ?: throw Exception("WTF")

由于操作符本身像是猫王头,所以叫Elvis操作符。。。这么形象的嘛

image.png

安全类型转换

在Kotlin中,进行强制类型转换提供了安全类型转换操作符as?,同时还有不安全的类型转换操作符as

as与Java中的强制类型转换很接近,如果转换的目标类型是无法转换的,会抛出异常

val a: String = "b"
val b = a as Int   // 无法转换

image.png

强制转换会直接抛出异常,属于类型转换异常null是不能直接强制转换成非空类型的

val a: String? = null
val b = a as String     // 抛出异常

image.png

这里依然是空指针异常,在项目中使用尤其需要注意,可能那个变量是可空类型,就代表其null的可能,如果直接强转为不可空类型,是有空指针异常的风险的,不可不防

如果想要更加稳妥地进行类型转换,这时候要用到as?,毕竟问了总比不问要更加靠谱

val a = null
val b = a as? String  // 为String?,失败null

使用这种类型转换的方式并不会抛出异常,最终它只会将类型定位成对应的可空类型,通常我们期待使用的都是某个类型的非空对象,因此在转换成功的情况下返回对应的类型,而在转换失败的时候则是返回null,取代了直接抛出异常导致程序停止,因此更加安全

非空断言

然而,还有一个非常不被推荐的操作,就是使用非空断言!!,这个符号代表你很确信它虽然可空,但此刻你有把握其绝不为空,出了事我担着,有了这样的觉悟,你可以用

val a: String? = null
println(a!!.length)   // 失误就完蛋

image.png

主要用于将可空的类型转换为非空,这是由你的分析和自信确保的,显然这个可靠程度嘛~~

筛选集合非空元素

集合过滤null有个便捷的方法,不过说到底也是多了一层封装

val rawList = arrayListOf<Int?>()
for (i in 0 .. 10) {
    if (i % 2 == 1) {
        rawList.add(i)
    } else {
        rawList.add(null)
    }
}
val resultList = rawList.filterNotNull()  // 筛除null,保留非空
println(resultList)

image.png

源码中不过还是在遍历,将非空元素填入新集合

3. 小结

Kotlin在空安全方面的处理大多是围绕着可空类型而来,因此对于可空类型的正确使用就可以充分发挥Kotlin对于潜在的空指针问题的检验
从我个人的使用来看,有一些地方是需要经常使用可空类型的,比如接收后端数据的POJO、资源引用、类型转换,主要是需要对于没有把握的数据,尤其不是自己创建的,接收其他接口的都属于不是很可信的,需要添加可空类型让编译器辅助我们进行判断,这样才能很好地减少bug
另外,发现Kotlin的语法糖真不少,对于不少常用的场景都进行了封装,然而自己却往往不知道上哪找,不知道有没有好的方法?

Get started with Kotlin | Kotlin (kotlinlang.org)