空指標檢查
這里先定義一個Study介面:
//定義Study介面
interface Study {
//readBooks()方法
fun readBooks()
//doHomeWork()方法
fun doHomeWork(){
println("I'm doing homework now.")
}
}
這里定義了學生類來實作介面:
//主函式
fun main() {
}
//Person類 有name和age欄位
open class Person(val name: String, val age: Int) {
}
//學生類 繼承了Person類 實作了Study介面
class Student(name: String, age: Int) : Person(name, age), Study {
override fun readBooks() {
println("$name is reading")
}
}
//定義了一個實作 Study介面中的方法的方法
fun doStudy(study: Study) {
study.readBooks()
study.doHomeWork()
}
我們來看這個方法:
fun doStudy(study: Study) {
study.readBooks()
study.doHomeWork()
}
這個方法一定安全嗎,如果我們向doStudy()傳入了一個null引數,那么這里毫無疑問會發生空指標例外,因此,更加穩妥的方法是:
fun doStudy(study: Study) {
if (study != null) {
study.readBooks()
study.doHomeWork()
}
}
這樣就可以保證不管傳入的引數是什么,這段代碼始終都是安全的.
可見,這么一小段代碼都有可能發生空指標例外,在一個大型專案中,想要完全避免非空指標幾乎是不可能的事情,這也是它高居各類崩潰排行榜首位的原因.

Kotlin給我們提供了一套可為空的型別:在類名的后面加上一個問號就行了.如Int表示不可為空的整型,而Int?表示可為空的整型.

現在代碼就可以正常編譯通過了,并且完全不會出現空指標例外.
判空輔助工具
?.運算子:當物件不為空時正常呼叫相應的方法,當物件為空時則什么都不去做
比如下面代碼:
fun main() {
val student = Student("Tom", 18)
if (student != null) {
student.doSomething()
}
}
可以簡化為:
val student = Student("Tom", 18)
student?.doSomething()
可以看見我們借助?.運算子就省去了if判斷陳述句,你現在可能覺得沒什么,但當以后進入大型專案的時候,這點就非常有用了.
?:運算子:這個運算子左右兩邊都接收一個運算式,如果左邊運算式不為空就回傳左邊運算式的結果,否則就回傳右邊運算式的結果.例如:
var c = if (a != null) {
a
} else {
b
}
這段代碼可以簡化為:
val c = a ?: b
接下來我們通過具體的例子來結合使用?.和?:運算子,從而加深你對他們的理解.
我們現在要撰寫一個函式用來獲取文本的長度,按照傳統的方法你可以這樣寫:
fun getTextLength(text: String?): Int {
if (text != null) {
return text.length
}
return 0
}
這段代碼可以簡化為:
fun getTextLength(text: String?) = text?.length ?: 0
由于text可能是空的,因此我們在呼叫它的length欄位時需要使用?.運算子,而當text為空時 text?.length會回傳一個null值,這個時候我們再去借助?:運算子使它回傳0.
不管Kotlin的空指標檢查機制也不是完美的, 有的時候我們邏輯上面已經將空指標例外處理了,但是Kotlin編譯器并不知道,這個時候它還是會編譯失敗.

因為printUpperCase()函式并不知道外部已經對content變數進行了非空檢查,在呼叫printUpperCase()方法室,還認為這里存在空指標例外,所有無法通過.
在這種情況下,我們可以使用非空斷言工具,寫法是在物件的后面加上!!
fun printUpperCase() {
val upperCase = content!!.toUpperCase()
println(upperCase)
}
這是一種有風險的寫法,表明自己非常確信這里的物件不為空.
最后我們來學習let輔助工具,let并不是關鍵字,也不是運算子,而是一個函式.這個函式提供了函式式API的編程介面,并且將原始呼叫物件作為引數傳遞到Lambdai運算式中.
示例代碼:
obj.let{
obj2 ->
//撰寫具體的業務邏輯
}
這里呼叫了obj物件的let函式, 然后再Lambda運算式中的代碼就會立即執行,并且這個obj物件還會作為引數傳入到Lambda運算式,不過為了防止變數重名,這里我將引數名改為了obj2,但實際上他們是同一個物件,這就是let函式的作用.
我們回到doStudy()函式里:
fun doStudy(study: Study?) {
study?.readBooks()
study?.doHomeWork()
}
雖然可以編譯通過,但這種方式有點啰嗦,這段代碼如果使用if陳述句就會變成:
fun doStudy(study: Study?) {
if (study != null) {
study.readBooks()
}
if (study != null) {
study.doHomeWork()
}
}
這就是說,我們沒進行一次if判斷就可以隨意呼叫study物件的任何方法就可以隨意呼叫study物件的任何方法, 但受限制與?.運算子, 現在沒呼叫一次study物件的方法都要進行一次if判斷.
接下來我們使用?.運算子結合let函式來對代碼進行優化:
fun doStudy(study: Study?) {
study?.let { study ->
study.readBooks()
study.doHomeWork()
}
}
?.運算子表示物件為空時什么也不做,物件不為空時就呼叫let函式,而let函式會將study物件本身作為引數傳遞到Lambda運算式中,此時物件肯定不為空,我們就可以放心得呼叫它的任意方法了.
Lambda運算式中,如果引數只有一個,則可以使用it關鍵字替代,所以代碼可以簡化為:
fun doStudy(study: Study?) {
study?.let {
it.readBooks()
it.doHomeWork()
}
}
let函式是可以處理全域變數的判空問題的,而if條件陳述句則無法做到這一點:
使用let不報錯:

使用if報錯:

之所以會報錯,是因為全域變數隨時都有可能被其他執行緒修改,即使做了非空判斷,也無法保證if陳述句中的study沒有空指標風險.從這一點也可以看出let函式的優勢.
謝謝觀看,一起進步鴨
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/231099.html
標籤:其他
