Kotlin 空指针检查
可空类型系统
Kotlin利用编译时判空检查的机制几乎杜绝了空指针异常。虽然编译时判空检查的机制有时候会导致代码变得比较难写,但是不用担心,Kotlin提供了一系列的辅助工具,让我们能轻松地处理各种判空情况。
Kotlin将空指针异常的检查提前到了编译时期,如果我们的程序存在空指针异常的风险,那么在编译的时候会直接报错,修正之后才能成功运行,这样就可以保证程序在运行时期不会出现空指针异常了。看到这里,你可能产生了巨大的疑惑,所有的参数和变量都不可为空?这可真是前所未闻的事情,那如果我们的业务逻辑就是需要某个参数或者变量为空该怎么办呢?不用担心,Kotlin提供了另外一套可为空的类型系统,只不过在使用可为空的类型系统时,我们需要在编译时期就将所有潜在的空指针异常都处理掉,否则代码将无法编译通过。那么可为空的类型系统是什么样的呢?很简单,就是在类名的后面加上一个问号。比如,Int表示不可为空的整型,而Int?就表示可为空的整型;String表示不可为空的字符串,而String?就表示可为空的字符串。
1 2 3 4 5 6 7 8 9 10 | fun main() { doStudy( null ); } fun doStudy(study: Study?) { if (study != null ) { study.readBooks() study.doHomework() } } |
现在代码就可以正常编译通过了,并且还能保证完全不会出现空指针异常。
判空辅助工具
为了在编译时期就处理掉所有的空指针异常,通常需要编写很多额外的检查代码才行。如果每处检查代码都使用if判断语句,则会让代码变得比较啰嗦,而且if判断语句还处理不了全局变量的判空问题。为此,Kotlin专门提供了一系列的辅助工具,使开发者能够更轻松地进行判空处理
?.操作符
这个操作符的作用非常好理解,就是当对象不为空时正常调用相应的方法,当对象为空时则什么都不做。比如以下的判空处理代码:
1 2 3 | if (a != null ) { a.doSomething() } |
这段代码使用?.操作符就可以简化成:
1 | a?.doSomething() |
了解了?.操作符的作用,下面我们来看一下如何使用这个操作符对doStudy()函数进行优化,代码如下所示:
1 2 3 4 | fun doStudy(study: Study?) { 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是可能为空的,因此在调用它的length字段时需要使用?.操作符,而当text为空时,text?.length会返回一个null值,这个时候我们再借助?:操作符让它返回0。
非空断言工具 !!
不过Kotlin的空指针检查机制也并非总是那么智能,有的时候我们可能从逻辑上已经将空指针异常处理了,但是Kotlin的编译器并不知道,这个时候它还是会编译失败。观察如下的代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 | var content: String? = "hello" fun main() { if (content != null ) { printUpperCase() } } fun printUpperCase() { val upperCase = content.toUpperCase() println(upperCase) } |
看上去好像逻辑没什么问题,但是很遗憾,这段代码一定是无法运行的。因为printUpperCase()函数并不知道外部已经对content变量进行了非空检查,在调用toUpperCase()方法时,还认为这里存在空指针风险,从而无法编译通过。
在这种情况下,如果我们想要强行通过编译,可以使用非空断言工具,写法是在对象的后面加上!!,如下所示:
1 2 3 4 | fun printUpperCase() { val upperCase = content!!.toUpperCase() println(upperCase) } |
这是一种有风险的写法,意在告诉Kotlin,我非常确信这里的对象不会为空,所以不用你来帮我做空指针检查了,如果出现问题,你可以直接抛出空指针异常,后果由我自己承担。
辅助工具 let
let既不是操作符,也不是什么关键字,而是一个函数。这个函数提供了函数式API的编程接口,并将原始调用对象作为参数传递到Lambda表达式中。示例代码如下:
1 2 3 | obj.let { obj2 -> // 编写具体的业务逻辑 } |
这里调用了obj对象的let函数,然后Lambda表达式中的代码就会立即执行,并且这个obj对象本身还会作为参数传递到Lambda表达式中。不过,为了防止变量重名,这里我将参数名改成了obj2,但实际上它们是同一个对象,这就是let函数的作用。
let函数的特性配合?.操作符可以在空指针检查的时候起到很大的作用。
1 2 3 4 5 6 | fun doStudy(study: Study?) { study?.let { stu -> stu.readBooks() stu.doHomework() } } |
?.操作符表示对象为空时什么都不做,对象不为空时就调用let函数,而let函数会将study对象本身作为参数传递到Lambda表达式中,此时的study对象肯定不为空了,我们就能放心地调用它的任意方法了。
当Lambda表达式的参数列表中只有一个参数时,可以不用声明参数名,直接使用it关键字来代替即可,那么代码就可以进一步简化成:
1 2 3 4 5 6 | fun doStudy(study: Study?) { study?.let { it.readBooks() it.doHomework() } } |
let函数是可以处理全局变量的判空问题的,而if判断语句则无法做到这一点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | var content: String? = "hello" fun main() { if (content != null ) { printUpperCase() } } fun printUpperCase() { content?.let { val upperCase = it.toUpperCase() println(upperCase) } } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
2021-01-13 MySQL MyISAM表锁
2021-01-13 MySQL锁概述
2019-01-13 springboot集成mybatis
2019-01-13 idea搭建springboot
2017-01-13 mui 对话框 点击按钮不关闭对话框的办法
2017-01-13 CSS 背景(css background)