快速入門 Kotlin 編程
面向物件編程
不同于面向程序的語言(比如 C 語言),面向物件的語言是可以創建類的,類就是對事物的一種封裝,
簡單概括一下,就是將事物封裝成具體的類,然后將事物所擁有的屬性和能力分別定義成類中的欄位和函式,接下來對類進行實體化,再根據具體的編程需求呼叫類中的欄位和方法即可,
類與物件
class Person {
var name = ""
var age = 0
fun eat() {
println(name + " is eating. He is " + age + " years old.")
}
}
val p = Person()
- Kotlin 中也是使用 class 關鍵字來宣告一個類的
- 使用 var 關鍵字創建了 name 和 age 這兩個欄位,這是因為我們需要在創建物件之后再指定具體的姓名和年齡,而如果使用 val 關鍵字的話,初始化之后就不能再重新賦值了
- Kotlin 中實體化一個類的方式和 Java 是基本類似的,只是去掉了 new 關鍵字而已
繼承與建構式
可以讓 Student 類去繼承 Person 類,這樣 Student 就自動擁有了 Person 中的欄位和函式,另外還可以定義自己獨有的欄位和函式,想要讓 Student 類繼承 Person 類,我們得做兩件事才行
- 在 Person 類的前面加上 open 關鍵字使其可以被繼承
- 在 Java 中繼承的關鍵字是 extends,而在 Kotlin 中變成了一個冒號
class Student : Person() {
var sno = ""
var grade = 0
}
繼承這里最麻煩的就是為什么 Person 類的后面要加上一對括號呢?Java 中繼承的時候并不需要括號,要知道為什么,我們要先來看一下 Kotlin 中建構式的一些知識,
任何一個面向物件的編程語言都會有建構式的概念,Kotlin 中也有,但是 Kotlin 將建構式分成了兩種:主建構式和次建構式,
主建構式將會是你最常用的建構式,每個類默認都會有一個不帶引數的主建構式,當然你也可以顯式地給它指明引數,主建構式的特點是沒有函式體,直接定義在類名的后面即可,比如下面這種寫法:
class Student(val sno: String, val grade: Int) : Person() {
}
到這里為止都還挺好理解的吧?但是這和那對括號又有什么關系呢?這就涉及了 Java 繼承特性中的一個規定,子類中的建構式必須呼叫父類中的建構式,這個規定在 Kotlin 中也要遵守,
那么回頭看一下 Student 類,現在我們宣告了一個主建構式,根據繼承特性的規定,子類的建構式必須呼叫父類的建構式,可是主建構式并沒有函式體,我們怎樣去呼叫父類的建構式呢?你可能會說,在 init 結構體中去呼叫不就好了,這或許是一種辦法,但絕對不是一種好辦法,因為在絕大多數的場景下,我們是不需要撰寫 init 結構體的,
Kotlin 當然沒有采用這種設計,而是用了另外一種簡單但是可能不太好理解的設計方式:括號,子類的主建構式呼叫父類中的哪個建構式,在繼承的時候通過括號來指定,因此再來看一遍這段代碼,你應該就能理解了吧,
class Student(val sno: String, val grade: Int) : Person() {
}
在這里,Person 類后面的一對空括號表示 Student 類的主建構式在初始化的時候會呼叫 Person 類的無引數建構式,即使在無引數的情況下,這對括號也不能省略,
而如果我們將 Person 改造一下,將姓名和年齡都放到主建構式當中,如下所示:
open class Person(val name: String, val age: Int) {
...
}
此時你的 Student 類一定會報錯,這里出現錯誤的原因也很明顯,Person 類后面的空括號表示要去呼叫 Person 類中無參的建構式,但是 Person 類現在已經沒有無參的建構式了,所以就提示了上述錯誤,
如果我們想解決這個錯誤的話,就必須給 Person 類的建構式傳入 name 和 age 欄位,可是Student 類中也沒有這兩個欄位呀,很簡單,沒有就加唄,我們可以在 Student 類的主建構式中加上 name 和 age 這兩個引數,再將這兩個引數傳給 Person 類的建構式,代碼如下所示:
class Student(val sno: String, val grade: Int, name: String, age: Int) Person(name, age) {
...
}
學到這里,我們就將 Kotlin 的主建構式基本掌握了,是不是覺得繼承時的這對括號問題也不是那么難以理解?但是,Kotlin 在括號這個問題上的復雜度并不僅限于此,因為我們還沒涉及 Kotlin 建構式中的另一個組成部分——次建構式,
其實你幾乎是用不到次建構式的,Kotlin 提供了一個給函式設定引數默認值的功能,基本上可以替代次建構式的作用,我們會在本章最后學習這部分內容,但是考慮到知識結構的完整性,我決定還是介紹一下次建構式的相關知識,順便探討一下括號問題在次建構式上的區別,
你要知道,任何一個類只能有一個主建構式,但是可以有多個次建構式,次建構式也可以用于實體化一個類,這一點和主建構式沒有什么不同,只不過它是有函式體的,
Kotlin 規定,當一個類既有主建構式又有次建構式時,所有的次建構式都必須呼叫主建構式(包括間接呼叫),這里我通過一個具體的例子就能簡單闡明,代碼如下:
class Student(val sno: String, val grade: Int, name: String, age: Int) : Person(name, age) {
constructor(name: String, age: Int) : this("001", 1, name, age)
constructor() : this("geely", 24)
}
次建構式是通過 constructor 關鍵字來定義的,這里我們定義了兩個次建構式:第一個次建構式接收 name 和 age 引數,然后它又通過 this 關鍵字呼叫了主建構式,并將 sno 和 grade 這兩個引數賦值成初始值;第二個次建構式不接收任何引數,它通過 this 關鍵字呼叫了我們剛才定義的第一個次建構式,并將 name 和 age 引數也賦值成初始值,由于第二個次建構式間接呼叫了主建構式,因此這仍然是合法的,
那么現在我們就擁有了 3 種方式來對 Student 類進行物體化,分別是通過不帶引數的建構式、通過帶兩個引數的建構式和通過帶 4 個引數的建構式,對應代碼如下所示:
val student1 = Student()
val student2 = Student("Jack", 19)
val student3 = Student("a123", 5, "Jack", 19)
這樣我們就將次建構式的用法掌握得差不多了,但是到目前為止,繼承時的括號問題還沒有進一步延伸,暫時和之前學過的場景是一樣的,
那么接下來我們就再來看一種非常特殊的情況:類中只有次建構式,沒有主建構式,這種情況真的十分少見,但在 Kotlin 中是允許的,當一個類沒有顯式地定義主建構式且定義了次建構式時,它就是沒有主建構式的,我們結合代碼來看一下:
class Student : Person {
constructor(name: String, age: Int) : super(name, age) {
}
}
注意這里的代碼變化,首先 Student 類的后面沒有顯式地定義主建構式,同時又因為定義了次建構式,所以現在 Student 類是沒有主建構式的,那么既然沒有主建構式,繼承 Person 類的時候也就不需要再加上括號了,其實原因就是這么簡單,只是很多人在剛開始學習 Kotlin 的時候沒能理解這對括號的意義和規則,因此總感覺繼承的寫法有時候要加上括號,有時候又不要加,搞得暈頭轉向的,而在你真正理解了規則之后,就會發現其實還是很好懂的,
另外,由于沒有主建構式,次建構式只能直接呼叫父類的建構式,上述代碼也是將 this 關鍵字換成了 super 關鍵字,這部分就很好理解了,因為和 Java 比較像,我也就不再多說了,
這一小節我們對 Kotlin 的繼承和建構式的問題探究得比較深,同時這也是很多人新上手 Kotlin 時比較難理解的部分,希望你能好好掌握這部分內容,
介面
Java 中繼承使用的關鍵字是 extends,實作介面使用的關鍵字是 implements,而 Kotlin 中統一使用冒號,中間用逗號進行分隔,另外介面的后面不用加上括號,因為它沒有建構式可以去呼叫,我們來看下代碼:
class Student(name: String, age: Int) : Person(name, age), Study {
override fun readBooks() {
println(name + " is reading.")
}
override fun doHomework() {
println(name + " is doing homework.")
}
}
資料類與單例類
data class Cellphone(val brand: String, val price: Double)
利用 Kotlin 創建資料類非常簡單,只需要一行代碼就可以了,你沒看錯,只需要一行代碼就可以實作了!神奇的地方就在于 data 這個關鍵字,當在一個類前面宣告了 data 關鍵字時,就表明你希望這個類是一個資料類,Kotlin 會根據主建構式中的引數幫你將 equals()、hashCode()、toString() 等固定且無實際邏輯意義的方法自動生成,從而大大減少了開發的作業量,
在 Kotlin 中創建一個單例類的方式極其簡單,只需要將 class 關鍵字改成 object 關鍵字即可,
object Singleton {
fun singletonTest() {
println("singletonTest is called.")
}
}
可以看到,在 Kotlin 中我們不需要私有化建構式,也不需要提供 getInstance() 這樣的靜態方法,只需要把 class 關鍵字改成 object 關鍵字,一個單例類就創建完成了,而呼叫單例類中的函式也很簡單,比較類似于 Java 中靜態方法的呼叫方式:
Singleton.singletonTest()
這種寫法雖然看上去像是靜態方法的呼叫,但其實 Kotlin 在背后自動幫我們創建了一個 Singleton 類的實體,并且保證全域只會存在一個 Singleton 實體,
Lambda 編程
Kotlin 從第一個版本開始就支持了 Lambda 編程,并且 Kotlin 中的 Lambda 功能極為強大,我甚至認為 Lambda 才是 Kotlin 的靈魂所在,
不過,本章只是 Kotlin 的入門章節,我不可能在這短短一節里就將 Lambda 的方方面面全部覆寫,因此,這一節我們只學習一些 Lambda 編程的基礎知識,而像高階函式、DSL 等高級 Lambda 技巧,我們會在本書的后續章節慢慢學習,
集合的創建與遍歷
集合的函式式 API 是用來入門 Lambda 編程的絕佳示例,不過在此之前,我們得先學習創建集合的方式才行,
Kotlin 專門提供了一個內置的 listOf() 函式來簡化初始化集合的寫法,如下所示:
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
for-in 回圈不僅可以用來遍歷區間,還可以用來遍歷集合,現在我們就嘗試一下使用 for-in 回圈來遍歷這個水果集合,在 main() 函式中撰寫如下代碼:
fun main() {
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
for (fruit in list) {
println(fruit)
}
}
Map 是一種鍵值對形式的資料結構,因此在用法上和 List、Set 集合有較大的不同,
Kotlin 中并不建議使用 put() 和 get() 方法來對 Map 進行添加和讀取資料操作,而是更加推薦使用一種類似于陣列下標的語法結構,比如向 Map 中添加一條資料就可以這么寫:
map["Apple"] = 1
而從 Map 中讀取一條資料就可以這么寫:
val number = map["Apple"]
當然,這仍然不是最簡便的寫法,因為 Kotlin 毫無疑問地提供了一對 mapOf() 和 mutableMapOf() 函式來繼續簡化 Map 的用法,在 mapOf() 函式中,我們可以直接傳入初始化的鍵值對組合來完成對 Map 集合的創建:
val map = mapOf("Apple" to 1, "Banana" to 2, "Orange" to 3, "Pear" to 4, "Grape" to 5)
集合的函式式 API
集合的函式式 API 有很多個,這里我并不打算帶你涉獵所有函式式 API 的用法,而是重點學習函式式 API 的語法結構,也就是 Lambda 運算式的語法結構,
首先我們來思考一個需求,如何在一個水果集合里面找到單詞最長的那個水果?當然這個需求很簡單,也有很多種寫法,你可能會很自然地寫出如下代碼:
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon")
var maxLengthFruit = ""
for (fruit in list) {
if (fruit.length > maxLengthFruit.length) {
maxLengthFruit = fruit
}
}
println("max length fruit is $maxLengthFruit")
這段代碼很簡潔,思路也很清晰,可以說是一段相當不錯的代碼了,但是如果我們使用集合的函式式 API,就可以讓這個功能變得更加容易:
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon")
val maxLengthFruit = list.maxBy{it.length}
prinln("max length fruit is $maxLengthFruit")
上述代碼使用的就是函式式 API 的用法,只用一行代碼就能找到集合中單詞最長的那個水果,或許你現在理解這段代碼還比較吃力,那是因為我們還沒有開始學習 Lambda 運算式的語法結構,等學完之后再來重新看這段代碼時,你就會覺得非常簡單易懂了,
首先來看一下 Lambda 的定義,如果用最直白的語言來闡述的話,Lambda 就是一小段可以作為引數傳遞的代碼,從定義上看,這個功能就很厲害了,因為正常情況下,我們向某個函式傳參時只能傳入變數,而借助 Lambda 卻允許傳入一小段代碼,這里兩次使用了“一小段代碼”這種描述,那么到底多少代碼才算一小段代碼呢?Kotlin 對此并沒有進行限制,但是通常不建議在 Lambda 運算式中撰寫太長的代碼,否則可能會影響代碼的可讀性,
接著我們來看一下 Lambda 運算式的語法結構:
{引數名1: 引數型別, 引數名2: 引數型別 -> 函式體}
這是 Lambda 運算式最完整的語法結構定義,首先最外層是一對大括號,如果有引數傳入到 Lambda 運算式中的話,我們還需要宣告引數串列,引數串列的結尾使用一個 -> 符號,表示引數串列的結束以及函式體的開始,函式體中可以撰寫任意行代碼(雖然不建議撰寫太長的代碼),并且最后一行代碼會自動作為 Lambda 運算式的回傳值,
當然,在很多情況下,我們并不需要使用 Lambda 運算式完整的語法結構,而是有很多種簡化的寫法,那么接下來我們就由繁入簡開始吧,
還是回到剛才找出最長單詞水果的需求,前面使用的函式式 API 的語法結構看上去好像很特殊,但其實 maxBy 就是一個普通的函式而已,只不過它接收的是一個 Lambda 型別的引數,并且會在遍歷集合時將每次遍歷的值作為引數傳遞給 Lambda 運算式,maxBy 函式的作業原理是根據我們傳入的條件來遍歷集合,從而找到該條件下的最大值,比如說想要找到單詞最長的水果,那么條件自然就應該是單詞的長度了,
理解了 maxBy 函式的作業原理之后,我們就可以開始套用剛才學習的 Lambda 運算式的語法結構,并將它傳入到 maxBy 函式中了,如下所示:
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon")
val lambda = {fruit: String -> fruit.length}
val maxLengthFruit = list.maxBy(lambda)
可以看到,maxBy 函式實質上就是接收了一個 Lambda 引數而已,并且這個 Lambda 引數是完全按照剛才學習的運算式的語法結構來定義的,因此這段代碼應該算是比較好懂的,
這種寫法雖然可以正常作業,但是比較啰嗦,可簡化的點也非常多,下面我們就開始對這段代碼一步步進行簡化,
- 首先,我們不需要專門定義一個 lambda 變數,而是可以直接將 lambda 運算式傳入 maxBy 函式當中,因此第一步簡化如下所示:
val maxLengthFruit = list.maxBy({ fruit: String -> fruit.length }) - 然后 Kotlin 規定,當 Lambda 引數是函式的最后一個引數時,可以將 Lambda 運算式移到函式括號的外面,如下所示:
val maxLengthFruit = list.maxBy() { fruit: String -> fruit.length } - 接下來,如果 Lambda 引數是函式的唯一一個引數的話,還可以將函式的括號省略:
val maxLengthFruit = list.maxBy{ fruit: String -> fruit.length } - 這樣代碼看起來就變得清爽多了吧?但是我們還可以繼續進行簡化,由于 Kotlin 擁有出色的型別推導機制,Lambda 運算式中的引數串列其實在大多數情況下不必宣告引數型別,因此代碼可以進一步簡化成:
val maxLengthFruit = list.maxBy{ fruit -> fruit.length } - 最后,當 Lambda 運算式的引數串列中只有一個引數時,也不必宣告引數名,而是可以使用 it 關鍵字來代替,那么代碼就變成了:
val maxLengthFruit = list.maxBy { it.length }
怎么樣?通過一步步推導的方式,我們就得到了和一開始那段函式式 API 一模一樣的寫法,是不是現在理解起來就非常輕松了呢?
接下來我們就再來學習幾個集合中比較常用的函式式 API,相信這些對于現在的你來說,應該是沒有什么困難的,
集合中的 map 函式是最常用的一種函式式 API,它用于將集合中的每個元素都映射成一個另外的值,映射的規則在 Lambda 運算式中指定,最終生成一個新的集合,比如,這里我們希望讓所有的水果名都變成大寫模式,就可以這樣寫:
fun main() {
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon")
val newList = list.map { it.toUpperCase() }
for (fruit in newList) {
println(fruit)
}
}
map 函式的功能非常強大,它可以按照我們的需求對集合中的元素進行任意的映射轉換,上面只是一個簡單的示例而已,除此之外,你還可以將水果名全部轉換成小寫,或者是只取單詞的首字母,甚至是轉換成單詞長度這樣一個數字集合,只要在 Lambda 表示式中撰寫你需要的邏輯即可,
接下來我們再來學習另外一個比較常用的函式式 API——filter 函式,顧名思義,filter 函式是用來過濾集合中的資料的,它可以單獨使用,也可以配合剛才的 map 函式一起使用,
fun main() {
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon")
val newList = list.filter { it.length <= 5 }
.map { it.toUpperCase() }
for (fruit in newList) {
println(fruit)
}
}
接下來我們繼續學習兩個比較常用的函式式 API——any 和 all 函式,其中 any 函式用于判斷集合中是否至少存在一個元素滿足指定條件,all 函式用于判斷集合中是否所有元素都滿足指定條件,由于這兩個函式都很好理解,我們就直接通過代碼示例學習了:
fun main() {
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon")
val anyResult = list.any { it.length <= 5 }
val allResult = list.all { it.length <= 5 }
println("anyResult is " + anyResult + ", allResult is " + allResult)
}
這樣我們就將 Lambda 運算式的語法結構和幾個常用的函式式 API 的用法都學習完了,雖然集合中還有許多其他函式式 API,但是只要掌握了基本的語法規則,其他函式式 API 的用法只要看一看檔案就能掌握了,相信這對你來說并不是難事,
空指標檢查
Android 系統上崩潰率最高的例外型別就是空指標例外(NullPointerException),相信不只是 Android,其他系統上也面臨著相同的問題,若要分析其根本原因的話,我覺得主要是因為空指標是一種不受編程語言檢查的運行時例外,只能由程式員主動通過邏輯判斷來避免,但即使是最出色的程式員,也不可能將所有潛在的空指標例外全部考慮到,
我們來看一段非常簡單的 Java 代碼:
public void doStudy(Study study) {
study.readBooks();
study.doHomework();
}
這是我們前面撰寫過的一個 doStudy() 方法,我將它翻譯成了 Java 版,這段代碼沒有任何復雜的邏輯,只是接收了一個 Study 引數,并且呼叫了引數的 readBooks() 和 doHomework() 方法,
這段代碼安全嗎?不一定,因為這要取決于呼叫方傳入的引數是什么,如果我們向 doStudy() 方法傳入了一個 null 引數,那么毫無疑問這里就會發生空指標例外,因此,更加穩妥的做法是在呼叫引數的方法之前先進行一個判空處理,如下所示:
public void doStudy(Study study) {
if (study != null) {
study.readBooks();
study.doHomework();
}
}
這樣就能保證不管傳入的引數是什么,這段代碼始終都是安全的,
由此可以看出,即使是如此簡單的一小段代碼,都有產生空指標例外的潛在風險,那么在一個大型專案中,想要完全規避空指標例外幾乎是不可能的事情,這也是它高居各類崩潰排行榜首位的原因,
可空型別系統
然而,Kotlin 卻非常科學地解決了這個問題,它利用編譯時判空檢查的機制幾乎杜絕了空指標例外,雖然編譯時判空檢查的機制有時候會導致代碼變得比較難寫,但是不用擔心,Kotlin 提供了一系列的輔助工具,讓我們能輕松地處理各種判空情況,下面我們就逐步開始學習吧,
還是回到剛才的 doStudy() 函式,現在將這個函式再翻譯回 Kotlin 版本,代碼如下所示:
fun doStudy(study: Study) {
study.readBooks()
study.doHomework()
}
這段代碼看上去和剛才的 Java 版本并沒有什么區別,但實際上它是沒有空指標風險的,因為 Kotlin默認所有的引數和變數都不可為空,所以這里傳入的 Study 引數也一定不會為空,我們可以放心地呼叫它的任何函式,如果你嘗試向 doStudy() 函式傳入一個 null 引數,則會提示如下圖所示的錯誤,

看到這里,你可能產生了巨大的疑惑,所有的引數和變數都不可為空?這可真是前所未聞的事情,那如果我們的業務邏輯就是需要某個引數或者變數為空該怎么辦呢?不用擔心,Kotlin 提供了另外一套可為空的型別系統,只不過在使用可為空的型別系統時,我們需要在編譯時期就將所有潛在的空指標例外都處理掉,否則代碼將無法編譯通過,
那么可為空的型別系統是什么樣的呢?很簡單,就是在類名的后面加上一個問號,比如,Int 表示不可為空的整型,而 Int? 就表示可為空的整型;String 表示不可為空的字串,而 String? 就表示可為空的字串,
回到剛才的 doStudy() 函式,如果我們希望傳入的引數可以為空,那么就應該將引數的型別由 Study 改成 Study?,如下圖所示,

可以看到,現在在呼叫 doStudy() 函式時傳入 null 引數,就不會再提示錯誤了,然而你會發現,在 doStudy() 函式中呼叫引數的 readBooks() 和 doHomework() 方法時,卻出現了一個紅色下滑線的錯誤提示,這又是為什么呢?
其實原因也很明顯,由于我們將引數改成了可為空的 Study? 型別,此時呼叫引數的 readBooks() 和 doHomework() 方法都可能造成空指標例外,因此 Kotlin 在這種情況下不允許編譯通過,
那么該如何解決呢?很簡單,只要把空指標例外都處理掉就可以了,比如做個判斷處理,如下
所示:
fun doStudy(study: Study?) {
if (study != null) {
study.readBooks()
study.doHomework()
}
}
現在代碼就可以正常編譯通過了,并且還能保證完全不會出現空指標例外,
其實學到這里,我們就已經基本掌握了 Kotlin 的可空型別系統以及空指標檢查的機制,但是為了在編譯時期就處理掉所有的空指標例外,通常需要撰寫很多額外的檢查代碼才行,如果每處檢查代碼都使用 if 判斷陳述句,則會讓代碼變得比較啰嗦,而且 if 判斷陳述句還處理不了全域變數的判空問題,為此,Kotlin 專門提供了一系列的輔助工具,使開發者能夠更輕松地進行判空處理,下面我們就來逐個學習一下,
判空輔助工具
首先學習最常用的 ?. 運算子,這個運算子的作用非常好理解,就是當物件不為空時正常呼叫相應的方法,當物件為空時則什么都不做,比如以下的判空處理代碼:
if (a != null) {
a.doSomething()
}
這段代碼使用 ?. 運算子就可以簡化成:
a?.doSomething()
了解了 ?. 運算子的作用,下面我們來看一下如何使用這個運算子對 doStudy() 函式進行優化,代碼如下所示:
fun doStudy(study: Study?) {
study?.readBooks()
study?.doHomework()
}
下面我們再來學習另外一個非常常用的 ?: 運算子,這個運算子的左右兩邊都接收一個運算式,如果左邊運算式的結果不為空就回傳左邊運算式的結果,否則就回傳右邊運算式的結果,觀察如下代碼:
val c = if (a ! = null) {
a
} else {
b
}
這段代碼的邏輯使用 ?: 運算子就可以簡化成:
val c = a ?: b
接下來我們通過一個具體的例子來結合使用 ?. 和 ?: 這兩個運算子,從而讓你加深對它們的理解,
比如現在我們要撰寫一個函式用來獲得一段文本的長度,使用傳統的寫法就可以這樣寫:
fun getTextLength(text: String?): Int {
if (text != null) {
return text.length
}
return 0
}
由于文本是可能為空的,因此我們需要先進行一次判空操作,如果文本不為空就回傳它的長度,如果文本為空就回傳 0,
這段代碼看上去也并不復雜,但是我們卻可以借助運算子讓它變得更加簡單,如下所示:
fun getTextLength(text: String?) = text?.length ?: 0
這里我們將 ?. 和 ?: 運算子結合到了一起使用,首先由于 text 是可能為空的,因此我們在呼叫它的 length 欄位時需要使用 ?. 運算子,而當 text 為空時,text?.length 會回傳一個 null 值,這個時候我們再借助 ?: 運算子讓它回傳 0,怎么樣,是不是覺得這些運算子越來越好用了呢?
不過 Kotlin 的空指標檢查機制也并非總是那么智能,有的時候我們可能從邏輯上已經將空指標例外處理了,但是 Kotlin 的編譯器并不知道,這個時候它還是會編譯失敗,
觀察如下的代碼示例:
var content: String? = "hello"
fun main() {
if (content != null) {
printUpperCase()
}
}
fun printUpperCase() {
val upperCase = content.toUpperCase()
println(upperCase)
}
這里我們定義了一個可為空的全域變數 content,然后在 main() 函式里先進行一次判空操作,當 content 不為空的時候才會呼叫 printUpperCase() 函式,在 printUpperCase() 函式里,我們將 content 轉換為大寫模式,最后列印出來,
看上去好像邏輯沒什么問題,但是很遺憾,這段代碼一定是無法運行的,因為 printUpperCase() 函式并不知道外部已經對 content 變數進行了非空檢查,在呼叫 toUpperCase() 方法時,還認為這里存在空指標風險,從而無法編譯通過,
在這種情況下,如果我們想要強行通過編譯,可以使用非空斷言工具,寫法是在物件的后面加上 !!,如下所示:
fun printUpperCase() {
val upperCase = content!!.toUpperCase()
println(upperCase)
}
這是一種有風險的寫法,意在告訴 Kotlin,我非常確信這里的物件不會為空,所以不用你來幫我做空指標檢查了,如果出現問題,你可以直接拋出空指標例外,后果由我自己承擔,
最后我們再來學習一個比較與眾不同的輔助工具 ——let,let 既不是運算子,也不是什么關鍵字,而是一個函式,這個函式提供了函式式 API 的編程介面,并將原始呼叫物件作為引數傳遞到 Lambda 運算式中,示例代碼如下:
obj.let { obj2 ->
// 撰寫具體的業務邏輯
}
可以看到,這里呼叫了 obj 物件的 let 函式,然后 Lambda 運算式中的代碼就會立即執行,并且這個 obj 物件本身還會作為引數傳遞到 Lambda 運算式中,不過,為了防止變數重名,這里我將引數名改成了 obj2,但實際上它們是同一個物件,這就是 let 函式的作用,
let 函式屬于 Kotlin 中的標準函式,在下一章中我們將會學習更多 Kotlin 標準函式的用法,
你可能就要問了,這個 let 函式和空指標檢查有什么關系呢?其實 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 物件的方法時都要進行一次if判斷,
這個時候就可以結合使用 ?. 運算子和 let 函式來對代碼進行優化了,如下所示:
fun doStudy(study: Study?) {
study?.let { stu ->
stu.readBooks()
stu.doHomework()
}
}
我來簡單解釋一下上述代碼,?. 運算子表示物件為空時什么都不做,物件不為空時就呼叫 let 函式,而 let 函式會將 study 物件本身作為引數傳遞到 Lambda 運算式中,此時的 study 物件肯定不為空了,我們就能放心地呼叫它的任意方法了,
另外還記得 Lambda 運算式的語法特性嗎?當 Lambda 運算式的引數串列中只有一個引數時,可以不用宣告引數名,直接使用 it 關鍵字來代替即可,那么代碼就可以進一步簡化成:
fun doStudy(study: Study?) {
study?.let {
it.readBooks()
it.doHomework()
}
}
在結束本小節內容之前,我還得再講一點,let 函式是可以處理全域變數的判空問題的,而 if 判斷陳述句則無法做到這一點,比如我們將 doStudy( ) 函式中的引數變成一個全域變數,使用 let 函式仍然可以正常作業,但使用 if 判斷陳述句則會提示錯誤,如下圖所示,

之所以這里會報錯,是因為全域變數的值隨時都有可能被其他執行緒所修改,即使做了判空處理,仍然無法保證 if 陳述句中的 study 變數沒有空指標風險,從這一點上也能體現出 let 函式的優勢,
好了,最常用的 Kotlin 空指標檢查輔助工具大概就是這些了,只要能將本節的內容掌握好,你就可以寫出更加健壯、幾乎杜絕空指標例外的代碼了,
Kotlin 中的小魔術
字串模板
首先來看一下 Kotlin 中字串內嵌運算式的語法規則:
"hello, ${obj.name}. nice to meet you!"
可以看到,Kotlin 允許我們在字串里嵌入 ${} 這種語法結構的運算式,并在運行時使用運算式執行的結果替代這一部分內容,
函式的引數默認值
上述代碼中有一個主建構式和兩個次建構式,次建構式在這里的作用是提供了使用更少引數來對 Student 類進行實體化的方式,無參的次建構式會呼叫兩個引數的次建構式,并將這兩個引數賦值成初始值,兩個引數的次建構式會呼叫 4 個引數的主建構式,并將缺失的兩個引數也賦值成初始值,這種寫法在 Kotlin 中其實是不必要的,因為我們完全可以通過只撰寫一個主建構式,然后給引數設定默認值的方式來實作,代碼如下所示:
class Student(val sno: String = "", val grade: Int = 0, name: String = "", age: Int = 0) : Person(name, age) {
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/289980.html
標籤:其他
上一篇:身份證讀取設備開發解決方案:3、單片機讀取身份證資訊的demo
下一篇:如何修改軟體名子及圖示,非常詳細
