Kotlin基础学习3
写在前面
本文上接:Kotlin基础学习2,在之前学习了Koltin中的Lambda表达式后,今天来学习我感觉能够让Kotlin成为谷歌推荐的安卓推荐编程语言的原因——空指针检查。大伙儿在做安卓的时候,肯定能体会到NullPointerException的恐怖。遇到这种错误,总是不知道错在哪里。而Kotlin通过空指针检查基本解决了这一问题,今天就来学习一下空指针检查。
空指针检查
通常情况,我们在安卓开发中总是需要对某个控件或变量进行判空,以确保他不会造成空指针异常。但一旦项目的代码量上去了,总会有出错的时候。Kotlin通过在编译时对判空进行检查,虽然这么搞有时候会让代码很难写,但Kotlin还提供了很多的辅助工具方便处理。
可空类型系统
我们先来看一段代码:
fun doStudy(s:String){
s.toUpperCase()
s.toLowerCase()
}
这段看上去似乎有着空指针的风险,但实际上是没有的,因为Koltin默认所有的参数和变量都不可为空。所以这里传入的String参数一定不会为空,调用其中的方法自然没有任何问题。如果你尝试给他传入一个null参数,编译器会报错:
也就是说,Kotlin将空指针异常的检查提前到了编译时期,如果有空指针异常的风险直接不通过编译,更不要谈运行时报错了。这样就从根本上杜绝了空指针异常。
但你看到这里可能会觉得诧异,那我如果真的需要传入一个null值怎么办?Kotlin为我们提供了另外一套可为空的类型系统,即在变量类型后面加上问号即可,如:
fun doStudy(s:String?){
s.toUpperCase()
s.toLowerCase()
}
但这时候你会发现出现了红点:
很正常,因为此时我们的s是有可能为空的,必须排除他为空的可能,如下:
fun doStudy(s:String?){
if(s != null){
s.toUpperCase()
s.toLowerCase()
}
}
这样就完事了,但我们发现,好像和之前用Java没什么区别啊?我不是还得写一堆的判空语句吗。别急,下面来学习Kotlin提供的辅助工具来更轻松的进行判空。
判空辅助工具
-
首先来学习最常用的?.操作符,这个符号的意思很好理解——当对象不为空时调用相应的方法,当对象为空的时候就什么都不做,比如我们上面的判空语句:
fun doStudy(s:String?){ s?.toUpperCase() s?.toLowerCase() }
还是很简单的吧?有了他我们就可以极大的简化我们的代码了。
-
学习了?.后,再来学习一个也很常用的?:操作符,这个操作符左右都接收一个表达式,如果左边表达式的结果不为空则返回左边表达式的结果,否则就返回右边的结果,比如:
val c = if(a != null){ a }else{ b }
这段代码的逻辑用?:操作符就可以简化成:
val c = a ?: b
当然,我们也可以将?.和?:一起用起来:
fun getTextLength(text:String?) = text?.length ?: 0
这里使用了函数里单行代码的语法糖,并且结合了?.和?:。具体来看,当text为空时,就会返回0值,当text不为空时,就会返回它的长度length。
-
当然,有时候Kotlin的编译器并不是那么智能。有的时候我们已经从逻辑上将空指针异常处理了,但Kotlin的编译器不知道,这时候还是会编译失败。比如以下这段代码:
var content:String? = "hello" fun main(){ if(content != null){ printUpperCase() } } fun printUpperCase(){ val upperCase = content.toUpperCase() println(upperCase) }
这里我们定义了一个可为空的全局变量content,然后在主函数里对其进行了判空处理,当content不为空时才会调用下面的方法输出它的大写值。从逻辑上讲似乎没什么问题,但这段代码无法通过编译。因为printUpperCase()这个方法并不知道外部对content已经进行了判空处理,在调用toUpperCase()方法时还是会认为存在空指针异常。
这种情况下,我们可以使用非空断言工具,写法是在对象的后面加上!!,如:
fun printUpperCase(){ val upperCase = content!!.toUpperCase() println(upperCase) }
断言,意思就是告诉编译器,我非常确信这里不会为空,你就不用管了。但这种工具如果自己不够确信的情况下,还是不要随意使用的。万一一个不好,就出了空指针异常。
-
最后,学习一个函数let,通过这个函数的特性可以帮助我们简化空指针检查时的代码。let是一个函数,里面要传入一个Lambda表达式,且会把原始调用对象作为参数传递到Lambda表达式里。那么怎么用他来简化我们的代码呢?如下:
fun doStudy(s:String?){ s?.toUpperCase() s?.toLowerCase() }
这是我们之前的代码,我们使用let函数:
fun doStudy(s:String?){ s?.let{s -> s.toUpperCase() s.toLowerCase() } }
再根据之前学习的Lambda表达式的特性,代码进一步简化:
fun doStudy(s:String?){ s?.let{ it.toUpperCase() it.toLowerCase() } }
要解释的话其实很简单,当对象不为空的时调用let函数,此时的s对象一定不为空,自然可以随便调用里面的方法了。
let函数一般用在判断全局变量的时候。在对全局变量判空时,使用if并不能通过编译,因为全局变量的值随时可能被其他线程修改啊,即使做了判空处理也不能确保他没有空指针风险。
Kotlin中的小魔术
到这里,我们已经学习了很多Kotlin的基础知识了。最后,我把书上提到的Kotlin中的小魔术也记录一下,学会了这些也会提升我们的编码速度。
字符串内嵌表达式
一般,我们在Java中连接字符串都会使用+号,但这种方式太繁琐了,一不小心我们就会写错。但Kotlin支持使用字符串内嵌表达式来简化我们的操作。先来看语法规则:
"hello ${obj.name} nice to meet you"
可以看到,Kotlin允许我们在字符串中嵌入${}这种语法结构的表达式,并会自动替换这部分内容。如果学过web的话,会发现el表达式也是这样的写法。另外,当表达式中仅有一个变量时,还可以省略大括号:
"hello $name nice to meet you"
接下来我们直接在代码中使用呗,如下:
val brand = "Samsung"
val price = 1299.9
println("Cellphone(brand = $brand, price = $price)")
可以看到,无论是易读性还是易写性都上了一层楼,可以说十分方便了。
函数的参数默认值
如果我们学过C++的话,就会知道C++里是允许在函数上直接给变量一个默认值的。我们的Kotlin也是支持这种写法的。如:
fun printParams(num:Int,str:String = "hello"){
println("num is $num, str is $str")
}
这里我们就给str指定了默认值hello,当我们调用这个方法时:
printParams(123)
不传入str的值,输出的结果也会如我们所愿的。
这时候我们换换,给num一个默认值:
fun printParams(num:Int = 100,str:String){
println("num is $num, str is $str")
}
这时候我们怎么调用这个方法呢?像刚才那样的使用肯定是不行的,编译器会认为我们想把一个字符串赋值给第一个num参数,直接类型不匹配。这时候我们就可以使用键值对的写法了,如下:
printParams(str = "world")
这时候就无所谓先后顺序了,毕竟是按键值匹配的。
这里我们就提到最开始学习的次构造函数,我们说函数的参数默认值这个功能可以很大程度上替代次构造函数。为什么这么说呢?次构造函数的作用一般是什么呢?就是为类提供更多的赋值方式,方便调用。而我们直接在主构造函数上设置函数默认值能达到一样的效果,这时我们就可以使用任意传参组合了。
总结
仔细想想这一路的学习,会发现Kotlin借鉴了很多其他语言的优势,优化了Java的一些痛点。总的来说虽然Lambda表达式的使用和理解还是有些难度,但我想在日后更多的使用中会更加巩固这方面的知识吧。