【碼上開學】Kotlin 裡那些「更方便的」

扔物線發表於2019-08-23

本期作者:

視訊:扔物線(朱凱)

文章:Sinyu(沈新宇)

大家好,我是扔物線朱凱。這期是碼上開學的 Kotlin 基礎部分的第三篇(也是基礎部分的最後一篇):Kotlin 裡那些「更好用的」。老朋友話不多,先上視訊。

因為我一直沒有學會怎麼在掘金貼視訊,所以請點選 這裡 去嗶哩嗶哩看,或者點選 這裡 去 YouTube 看。

以下內容來自文章作者 Sinyu

在上期內容當中,我們介紹了 Kotlin 的那些與 Java 寫法不同的地方。這一期我們再進階一點,講一講 Kotlin 中那些「更方便的」用法。這些知識點在不知道之前,你也可以正常寫 Kotlin,但是在熟悉之後會讓你寫得更爽。

構造器

主構造器

我們之前已經瞭解了 Kotlin 中 constructor 的寫法:

?️
class User {
    var name: String
    constructor(name: String) {
        this.name = name
    }
}
複製程式碼

其實 Kotlin 中還有更簡單的方法來寫構造器:

?️
               ?       
class User constructor(name: String) {
    //                  ? 這裡與構造器中的 name 是同一個
    var name: String = name
}
複製程式碼

這裡有幾處不同點:

  • constructor 構造器移到了類名之後
  • 類的屬性 name 可以引用構造器中的引數 name

這個寫法叫「主構造器 primary constructor」。與之相對的在第二篇中,寫在類中的構造器被稱為「次構造器」。在 Kotlin 中一個類最多隻能有 1 個主構造器(也可以沒有),而次構造器是沒有個數限制。

主構造器中的引數除了可以在類的屬性中使用,還可以在 init 程式碼塊中使用:

?️
class User constructor(name: String) {
    var name: String
    init {
        this.name = name
    }
}
複製程式碼

其中 init 程式碼塊是緊跟在主構造器之後執行的,這是因為主構造器本身沒有程式碼體,init 程式碼塊就充當了主構造器程式碼體的功能。

另外,如果類中有主構造器,那麼其他的次構造器都需要通過 this 關鍵字呼叫主構造器,可以直接呼叫或者通過別的次構造器間接呼叫。如果不呼叫 IDE 就會報錯:

?️
class User constructor(var name: String) {
    constructor(name: String, id: Int) {
    // ?這樣寫會報錯,Primary constructor call expected
    }
}
複製程式碼

為什麼當類中有主構造器的時候就強制要求次構造器呼叫主構造器呢?

我們從主構造器的特性出發,一旦在類中宣告瞭主構造器,就包含兩點:

  • 必須性:建立類的物件時,不管使用哪個構造器,都需要主構造器的參與
  • 第一性:在類的初始化過程中,首先執行的就是主構造器

這也就是主構造器的命名由來。

當一個類中同時有主構造器與次構造器的時候,需要這樣寫:

?️
class User constructor(var name: String) {
                                   // ?  ? 直接呼叫主構造器
    constructor(name: String, id: Int) : this(name) {
    }
                                                // ? 通過上一個次構造器,間接呼叫主構造器
    constructor(name: String, id: Int, age: Int) : this(name, id) {
    }
}
複製程式碼

在使用次構造器建立物件時,init 程式碼塊是先於次構造器執行的。如果把主構造器看成身體的頭部,那麼 init 程式碼塊就是頸部,次構造器就相當於身體其餘部分。

細心的你也許會發現這裡又出現了 : 符號,它還在其他場合出現過,例如:

  • 變數的宣告:var id: Int
  • 類的繼承:class MainActivity : AppCompatActivity() {}
  • 介面的實現:class User : Impl {}
  • 匿名類的建立:object: ViewPager.SimpleOnPageChangeListener() {}
  • 函式的返回值:fun sum(a: Int, b: Int): Int

可以看出 : 符號在 Kotlin 中非常高頻出現,它其實表示了一種依賴關係,在這裡表示依賴於主構造器。

通常情況下,主構造器中的 constructor 關鍵字可以省略:

?️
class User(name: String) {
    var name: String = name
}
複製程式碼

但有些場景,constructor 是不可以省略的,例如在主構造器上使用「可見性修飾符」或者「註解」:

  • 可見性修飾符我們之前已經講過,它修飾普通函式與修飾構造器的用法是一樣的,這裡不再詳述:

    ?️
    class User private constructor(name: String) {
    //           ? 主構造器被修飾為私有的,外部就無法呼叫該構造器
    }
    複製程式碼
  • 關於註解的知識點,我們之後會講,這裡就不展開了

既然主構造器可以簡化類的初始化過程,那我們就幫人幫到底,送佛送到西,用主構造器把屬性的初始化也一併給簡化了。

主構造器裡宣告屬性

之前我們講了主構造器中的引數可以在屬性中進行賦值,其實還可以在主構造器中直接宣告屬性:

?️
           ?
class User(var name: String) {
}
// 等價於:
class User(name: String) {
  var name: String = name
}
複製程式碼

如果在主構造器的引數宣告時加上 var 或者 val,就等價於在類中建立了該名稱的屬性(property),並且初始值就是主構造器中該引數的值。

以上講了所有關於主構造器相關的知識,讓我們總結一下類的初始化寫法:

  • 首先建立一個 User 類:

    ?️
    class User {
    }
    
    複製程式碼
  • 新增一個引數為 nameid 的主構造器:

    ?️
    class User(name: String, id: String) {
    }
    
    複製程式碼
  • 將主構造器中的 nameid 宣告為類的屬性:

    ?️
    class User(val name: String, val id: String) {
    }
    
    複製程式碼
  • 然後在 init 程式碼塊中新增一些初始化邏輯:

    ?️
    class User(val name: String, val id: String) {
        init {
            ...
        }
    }
    
    複製程式碼
  • 最後再新增其他次構造器:

    ?️
    class User(val name: String, val id: String) {
        init {
            ...
        }
        
        constructor(person: Person) : this(person.name, person.id) {
        }
    }
    
    複製程式碼

    當一個類有多個構造器時,只需要把最基本、最通用的那個寫成主構造器就行了。這裡我們選擇將引數為 nameid 的構造器作為主構造器。

到這裡,整個類的初始化就完成了,類的初始化順序就和上面的步驟一樣。

除了構造器,普通函式也是有很多簡化寫法的。

函式簡化

使用 = 連線返回值

我們已經知道了 Kotlin 中函式的寫法:

?️
fun area(width: Int, height: Int): Int {
    return width * height
}

複製程式碼

其實,這種只有一行程式碼的函式,還可以這麼寫:

?️
                                      ?
fun area(width: Int, height: Int): Int = width * height

複製程式碼

{}return 沒有了,使用 = 符號連線返回值。

我們之前講過,Kotlin 有「型別推斷」的特性,那麼這裡函式的返回型別還可以隱藏掉:

?️
//                               ?省略了返回型別
fun area(width: Int, height: Int) = width * height

複製程式碼

不過,在實際開發中,還是推薦顯式地將返回型別寫出來,增加程式碼可讀性。

以上是函式有返回值時的情況,對於沒有返回值的情況,可以理解為返回值是 Unit

?️
fun sayHi(name: String) {
    println("Hi " + name)
}

複製程式碼

因此也可以簡化成下面這樣:

?️
                       ?
fun sayHi(name: String) = println("Hi " + name)

複製程式碼

簡化完函式體,我們再來看看前面的引數部分。

對於 Java 中的方法過載,我們都不陌生,那 Kolin 中是否有更方便的過載方式呢?接下來我們看看 Kotlin 中的「引數預設值」的用法。

引數預設值

Java 中,允許在一個類中定義多個名稱相同的方法,但是引數的型別或個數必須不同,這就是方法的過載:

☕️
public void sayHi(String name) {
    System.out.println("Hi " + name);
}

public void sayHi() {
    sayHi("world"); 
}

複製程式碼

在 Kotlin 中,也可以使用這樣的方式進行函式的過載,不過還有一種更簡單的方式,那就是「引數預設值」:

?️
                           ?
fun sayHi(name: String = "world") = println("Hi " + name)

複製程式碼

這裡的 world 是引數 name 的預設值,當呼叫該函式時不傳引數,就會使用該預設值。

這就等價於上面 Java 寫的過載方法,當呼叫 sayHi 函式時,引數是可選的:

?️
sayHi("kaixue.io")
sayHi() // 使用了預設值 "world"

複製程式碼

既然與過載函式的效果相同,那 Kotlin 中的引數預設值有什麼好處呢?僅僅只是少寫了一些程式碼嗎?

其實在 Java 中,每個過載方法的內部實現可以各不相同,這就無法保證過載方法內部設計上的一致性,而 Kotlin 的引數預設值就解決了這個問題。

不過引數預設值在呼叫時也不是完全可以放飛自我的。

來看下面這段程式碼,這裡函式中有預設值的引數在無預設值引數的前面:

?️
fun sayHi(name: String = "world", age: Int) {
    ...
}

sayHi(10)
//    ? 這時想使用預設值進行呼叫,IDE 會報以下兩個錯誤
// The integer literal does not conform to the expected type String
// No value passed for parameter 'age'

複製程式碼

這個錯誤就是告訴你引數不匹配,說明我們的「開啟方式」不對,其實 Kotlin 裡是通過「命名引數」來解決這個問題的。

命名引數

具體用法如下:

?️
fun sayHi(name: String = "world", age: Int) {
    ...
}
      ?   
sayHi(age = 21)

複製程式碼

在呼叫函式時,顯式地指定了引數 age 的名稱,這就是「命名引數」。Kotlin 中的每一個函式引數都可以作為命名引數。

再來看一個有非常多引數的函式的例子:

?️ 
fun sayHi(name: String = "world", age: Int, isStudent: Boolean = true, isFat: Boolean = true, isTall: Boolean = true) {
    ...
}

複製程式碼

當函式中有非常多的引數時,呼叫該函式就會寫成這樣:

?️
sayHi("world", 21, false, true, false)

複製程式碼

當看到後面一長串的布林值時,我們很難分清楚每個引數的用處,可讀性很差。通過命名引數,我們就可以這麼寫:

?️
sayHi(name = "wo", age = 21, isStudent = false, isFat = true, isTall = false)

複製程式碼

與命名引數相對的一個概念被稱為「位置引數」,也就是按位置順序進行引數填寫。

當一個函式被呼叫時,如果混用位置引數與命名引數,那麼所有的位置引數都應該放在第一個命名引數之前:

?️
fun sayHi(name: String = "world", age: Int) {
    ...
}

sayHi(name = "wo", 21) // ? IDE 會報錯,Mixing named and positioned arguments is not allowed
sayHi("wo", age = 21) // ? 這是正確的寫法

複製程式碼

講完了命名引數,我們再看看 Kotlin 中的另一種常見函式:巢狀函式。

本地函式(巢狀函式)

首先來看下這段程式碼,這是一個簡單的登入的函式:

?️
fun login(user: String, password: String, illegalStr: String) {
    // 驗證 user 是否為空
    if (user.isEmpty()) {
        throw IllegalArgumentException(illegalStr)
    }
    // 驗證 password 是否為空
    if (password.isEmpty()) {
        throw IllegalArgumentException(illegalStr)
    }
}

複製程式碼

該函式中,檢查引數這個部分有些冗餘,我們又不想將這段邏輯作為一個單獨的函式對外暴露。這時可以使用巢狀函式,在 login 函式內部宣告一個函式:

?️
fun login(user: String, password: String, illegalStr: String) {
           ? 
    fun validate(value: String, illegalStr: String) {
      if (value.isEmpty()) {
          throw IllegalArgumentException(illegalStr)
      }
    }
   ?
    validate(user, illegalStr)
    validate(password, illegalStr)
}

複製程式碼

這裡我們將共同的驗證邏輯放進了巢狀函式 validate 中,並且 login 函式之外的其他地方無法訪問這個巢狀函式。

這裡的 illegalStr 是通過引數的方式傳進巢狀函式中的,其實完全沒有這個必要,因為巢狀函式中可以訪問在它外部的所有變數或常量,例如類中的屬性、當前函式中的引數與變數等。

我們稍加改進:

?️
fun login(user: String, password: String, illegalStr: String) {
    fun validate(value: String) {
        if (value.isEmpty()) {
                                              ?
            throw IllegalArgumentException(illegalStr)
        }
    }
    ...
}

複製程式碼

這裡省去了巢狀函式中的 illegalStr 引數,在該巢狀函式內直接使用外層函式 login 的引數 illegalStr

上面 login 函式中的驗證邏輯,其實還有另一種更簡單的方式:

?️
fun login(user: String, password: String, illegalStr: String) {
    require(user.isNotEmpty()) { illegalStr }
    require(password.isNotEmpty()) { illegalStr }
}

複製程式碼

其中用到了 lambda 表示式以及 Kotlin 內建的 require 函式,這裡先不做展開,之後的文章會介紹。

字串

講完了普通函式的簡化寫法,Kotlin 中字串也有很多方便寫法。

字串模板

在 Java 中,字串與變數之間是使用 + 符號進行拼接的,Kotlin 中也是如此:

?️
val name = "world"
println("Hi " + name)

複製程式碼

但是當變數比較多的時候,可讀性會變差,寫起來也比較麻煩。

Java 給出的解決方案是 String.format

☕️
System.out.print(String.format("Hi %s", name));

複製程式碼

Kotlin 為我們提供了一種更加方便的寫法:

?️
val name = "world"
//         ? 用 '$' 符號加引數的方式
println("Hi $name")

複製程式碼

這種方式就是把 name 從後置改為前置,簡化程式碼的同時增加了字串的可讀性。

除了變數,$ 後還可以跟表示式,但表示式是一個整體,所以我們要用 {} 給它包起來:

?️
val name = "world"
println("Hi ${name.length}") 

複製程式碼

其實就跟四則運算的括號一樣,提高語法上的優先順序,而單個變數的場景可以省略 {}

字串模板還支援轉義字元,比如使用轉義字元 \n 進行換行操作:

?️
val name = "world!\n"
println("Hi $name") // ? 會多打一個空行

複製程式碼

字串模板的用法對於我們 Android 工程師來說,其實一點都不陌生。

首先,Gradle 所用的 Groovy 語言就已經有了這種支援:

def name = "world"
println "Hi ${name}"

複製程式碼

在 Android 的資原始檔裡,定義字串也有類似用法:

<string name="hi">Hi %s</string> 

複製程式碼
☕️
getString(R.id.hi, "world");

複製程式碼

raw string (原生字串)

有時候我們不希望寫過多的轉義字元,這種情況 Kotlin 通過「原生字串」來實現。

用法就是使用一對 """ 將字串括起來:

?️
val name = "world"
val myName = "kotlin"
           ?
val text = """
      Hi $name!
    My name is $myName.\n
"""
println(text)

複製程式碼

這裡有幾個注意點:

  • \n 並不會被轉義
  • 最後輸出的內容與寫的內容完全一致,包括實際的換行
  • $ 符號引用變數仍然生效

這就是「原生字串」。輸出結果如下:

      Hi world!
    My name is kotlin.\n

複製程式碼

但對齊方式看起來不太優雅,原生字串還可以通過 trimMargin() 函式去除每行前面的空格:

?️
val text = """
     ? 
      |Hi world!
    |My name is kotlin.
""".trimMargin()
println(text)

複製程式碼

輸出結果如下:

Hi world!
My name is kotlin.

複製程式碼

這裡的 trimMargin() 函式有以下幾個注意點:

  • | 符號為預設的邊界字首,前面只能有空格,否則不會生效
  • 輸出時 | 符號以及它前面的空格都會被刪除
  • 邊界字首還可以使用其他字元,比如 trimMargin("/"),只不過上方的程式碼使用的是引數預設值的呼叫方式

字串的部分就先到這裡,下面來看看陣列與集合有哪些更方便的操作。

陣列和集合

陣列與集合的操作符

在之前的文章中,我們已經知道了陣列和集合的基本概念,其實 Kotlin 中,還為我們提供了許多使陣列與集合操作起來更加方便的函式。

首先宣告如下 IntArrayList

?️
val intArray = intArrayOf(1, 2, 3)
val strList = listOf("a", "b", "c")

複製程式碼

接下來,對它們的操作函式進行講解:

  • forEach:遍歷每一個元素

    ?️
    //              ? lambda 表示式,i 表示陣列的每個元素
    intArray.forEach { i ->
        print(i + " ")
    }
    // 輸出:1 2 3 
    
    複製程式碼

    除了「lambda」表示式,這裡也用到了「閉包」的概念,這又是另一個話題了,這裡先不展開。

  • filter:對每個元素進行過濾操作,如果 lambda 表示式中的條件成立則留下該元素,否則剔除,最終生成新的集合

    ?️
    // [1, 2, 3]
          ⬇️
    //  {2, 3}
    
    //            ? 注意,這裡變成了 List
    val newList: List = intArray.filter { i ->
        i != 1 // ? 過濾掉陣列中等於 1 的元素
    }
    
    複製程式碼
  • map:遍歷每個元素並執行給定表示式,最終形成新的集合

    ?️
    //  [1, 2, 3]
           ⬇️
    //  {2, 3, 4}
    
    val newList: List = intArray.map { i ->
        i + 1 // ? 每個元素加 1
    }
    
    複製程式碼
  • flatMap:遍歷每個元素,併為每個元素建立新的集合,最後合併到一個集合中

    ?️
    //          [1, 2, 3]
                   ⬇️
    // {"2", "a" , "3", "a", "4", "a"}
    
    intArray.flatMap { i ->
        listOf("${i + 1}", "a") // ? 生成新集合
    }
    
    複製程式碼

關於為什麼陣列的 filter 之後變成 List,就留作思考題吧~

這裡是以陣列 intArray 為例,集合 strList 也同樣有這些操作函式。Kotlin 中還有許多類似的操作函式,這裡就不一一列舉了。

除了陣列和集合,Kotlin 中還有另一種常用的資料型別: Range

Range

在 Java 語言中並沒有 Range 的概念,Kotlin 中的 Range 表示區間的意思,也就是範圍。區間的常見寫法如下:

?️
              ?      ?
val range: IntRange = 0..1000 

複製程式碼

這裡的 0..1000 就表示從 0 到 1000 的範圍,包括 1000,數學上稱為閉區間 [0, 1000]。除了這裡的 IntRange ,還有 CharRange 以及 LongRange

Kotlin 中沒有純的開區間的定義,不過有半開區間的定義:

?️
                         ?
val range: IntRange = 0 until 1000 

複製程式碼

這裡的 0 until 1000 表示從 0 到 1000,但不包括 1000,這就是半開區間 [0, 1000) 。

Range 這個東西,天生就是用來遍歷的:

?️
val range = 0..1000
//     ? 預設步長為 1,輸出:0, 1, 2, 3, 4, 5, 6, 7....1000,
for (i in range) {
    print("$i, ")
}

複製程式碼

這裡的 in 關鍵字可以與 for 迴圈結合使用,表示挨個遍歷 range 中的值。關於 for 迴圈控制的使用,在本期文章的後面會做具體講解。

除了使用預設的步長 1,還可以通過 step 設定步長:

?️
val range = 0..1000
//               ? 步長為 2,輸出:0, 2, 4, 6, 8, 10,....1000,
for (i in range step 2) {
    print("$i, ")
}

複製程式碼

以上是遞增區間,Kotlin 還提供了遞減區間 downTo ,不過遞減沒有半開區間的用法:

?️
//            ? 輸出:4, 3, 2, 1, 
for (i in 4 downTo 1) {
    print("$i, ")
}

複製程式碼

其中 4 downTo 1 就表示遞減的閉區間 [4, 1]。這裡的 downTo 以及上面的 step 都叫做「中綴表示式」,之後的文章會做介紹。

Sequence

在上一期中我們已經熟悉了 Sequence 的基本概念,這次讓我們更加深入地瞭解 Sequence 序列的使用方式。

序列 Sequence 又被稱為「惰性集合操作」,關於什麼是惰性,我們通過下面的例子來理解:

?️
val sequence = sequenceOf(1, 2, 3, 4)
val result: List = sequence
    .map { i ->
        println("Map $i")
        i * 2 
    }
    .filter { i ->
        println("Filter $i")
        i % 3  == 0 
    }
?
println(result.first()) // ? 只取集合的第一個元素

複製程式碼
  • 惰性的概念首先就是說在「?」標註之前的程式碼執行時不會立即執行,它只是定義了一個執行流程,只有 result 被使用到的時候才會執行

  • 當「?」的 println 執行時資料處理流程是這樣的:

    • 取出元素 1 -> map 為 2 -> filter 判斷 2 是否能被 3 整除
    • 取出元素 2 -> map 為 4 -> filter 判斷 4 是否能被 3 整除
    • ...

    惰性指當出現滿足條件的第一個元素的時候,Sequence 就不會執行後面的元素遍歷了,即跳過了 4 的遍歷。

List 是沒有惰性的特性的:

?️
val list = listOf(1, 2, 3, 4)
val result: List = list
    .map { i ->
        println("Map $i")
        i * 2 
    }
    .filter { i ->
        println("Filter $i")
        i % 3  == 0 
    }
?
println(result.first()) // ? 只取集合的第一個元素

複製程式碼

包括兩點:

  • 宣告之後立即執行
  • 資料處理流程如下:
    • {1, 2, 3, 4} -> {2, 4, 6, 8}
    • 遍歷判斷是否能被 3 整除

Sequence 這種類似懶載入的實現有下面這些優點:

  • 一旦滿足遍歷退出的條件,就可以省略後續不必要的遍歷過程。
  • List 這種實現 Iterable 介面的集合類,每呼叫一次函式就會生成一個新的 Iterable,下一個函式再基於新的 Iterable 執行,每次函式呼叫產生的臨時 Iterable 會導致額外的記憶體消耗,而 Sequence 在整個流程中只有一個。

因此,Sequence 這種資料型別可以在資料量比較大或者資料量未知的時候,作為流式處理的解決方案。

條件控制

相比 Java 的條件控制,Kotlin 中對條件控制進行了許多的優化及改進。

if/else

首先來看下 Java 中的 if/else 寫法:

☕️
int max;
if (a > b) {
    max = a;
} else {
    max = b;
}

複製程式碼

在 Kotlin 中,這麼寫當然也可以,不過,Kotlin 中 if 語句還可以作為一個表示式賦值給變數:

?️
       ?
val max = if (a > b) a else b

複製程式碼

另外,Kotlin 中棄用了三元運算子(條件 ? 然後 : 否則),不過我們可以使用 if/else 來代替它。

上面的 if/else 的分支中是一個變數,其實還可以是一個程式碼塊,程式碼塊的最後一行會作為結果返回:

?️
val max = if (a > b) {
    println("max:a")
    a // ? 返回 a
} else {
    println("max:b")
    b // ? 返回 b
}

複製程式碼

when

在 Java 中,用 switch 語句來判斷一個變數與一系列值中某個值是否相等:

☕️
switch (x) {
    case 1: {
        System.out.println("1");
        break;
    }
    case 2: {
        System.out.println("2");
        break;
    }
    default: {
        System.out.println("default");
    }
}

複製程式碼

在 Kotlin 中變成了 when

?️
?
when (x) {
   ?
    1 -> { println("1") }
    2 -> { println("2") }
   ?
    else -> { println("else") }
}

複製程式碼

這裡與 Java 相比的不同點有:

  • 省略了 casebreak,前者比較好理解,後者的意思是 Kotlin 自動為每個分支加上了 break 的功能,防止我們像 Java 那樣寫錯
  • Java 中的預設分支使用的是 default 關鍵字,Kotlin 中使用的是 else

if/else 一樣,when 也可以作為表示式進行使用,分支中最後一行的結果作為返回值。需要注意的是,這時就必須要有 else 分支,使得無論怎樣都會有結果返回,除非已經列出了所有情況:

?️
val value: Int = when (x) {
    1 -> { x + 1 }
    2 -> { x * 2 }
    else -> { x + 5 }
}

複製程式碼

在 Java 中,當多種情況執行同一份程式碼時,可以這麼寫:

☕️
switch (x) {
    case 1:
    case 2: {
        System.out.println("x == 1 or x == 2");
        break;
    }
    default: {
        System.out.println("default");
    }
}

複製程式碼

而 Kotlin 中多種情況執行同一份程式碼時,可以將多個分支條件放在一起,用 , 符號隔開,表示這些情況都會執行後面的程式碼:

?️
when (x) {
    ?
    1, 2 -> print("x == 1 or x == 2")
    else -> print("else")
}

複製程式碼

when 語句中,我們還可以使用表示式作為分支的判斷條件:

  • 使用 in 檢測是否在一個區間或者集合中:

    ?️
    when (x) {
       ?
        in 1..10 -> print("x 在區間 1..10 中")
       ?
        in listOf(1,2) -> print("x 在集合中")
       ? // not in
        !in 10..20 -> print("x 不在區間 10..20 中")
        else -> print("不在任何區間上")
    }
    
    複製程式碼
  • 或者使用 is 進行特定型別的檢測:

    ?️
    val isString = when(x) {
        ?
        is String -> true
        else -> false
    }
    
    複製程式碼
  • 還可以省略 when 後面的引數,每一個分支條件都可以是一個布林表示式:

    ?️
    when {
       ?
        str1.contains("a") -> print("字串 str1 包含 a")
       ?
        str2.length == 3 -> print("字串 str2 的長度為 3")
    }
    
    複製程式碼

當分支的判斷條件為表示式時,哪一個條件先為 true 就執行哪個分支的程式碼塊。

for

我們知道 Java 對一個集合或陣列可以這樣遍歷:

☕️
int[] array = {1, 2, 3, 4};
for (int item : array) {
    ...
}

複製程式碼

而 Kotlin 中 對陣列的遍歷是這麼寫的:

?️
val array = intArrayOf(1, 2, 3, 4)
          ?
for (item in array) {
    ...
}

複製程式碼

這裡與 Java 有幾處不同:

  • 在 Kotlin 中,表示單個元素的 item ,不用顯式的宣告型別
  • Kotlin 使用的是 in 關鍵字,表示 itemarray 裡面的一個元素

另外,Kotlin 的 in 後面的變數可以是任何實現 Iterable 介面的物件。

在 Java 中,我們還可以這麼寫 for 迴圈:

☕️
for (int i = 0; i <= 10; i++) {
    // 遍歷從 0 到 10
}

複製程式碼

但 Kotlin 中沒有這樣的寫法,那應該怎樣實現一個 0 到 10 的遍歷呢?

其實使用上面講過的區間就可以實現啦,程式碼如下:

?️
for (i in 0..10) {
    println(i)
}

複製程式碼

try-catch

關於 try-catch 我們都不陌生,在平時開發中難免都會遇到異常需要處理,那麼在 Kotlin 中是怎樣處理的呢,先來看下 Kotlin 中捕獲異常的程式碼:

?️
try {
    ...
}
catch (e: Exception) {
    ...
}
finally {
    ...
}

複製程式碼

可以發現 Kotlin 異常處理與 Java 的異常處理基本相同,但也有幾個不同點:

  • 我們知道在 Java 中,呼叫一個丟擲異常的方法時,我們需要對異常進行處理,否則就會報錯:

    ☕️
    public class User{
        void sayHi() throws IOException {
        }
        
        void test() {
            sayHi();
            // ? IDE 報錯,Unhandled exception: java.io.IOException
        }
    }
    
    複製程式碼

    但在 Kotlin 中,呼叫上方 User 類的 sayHi 方法時:

    ?️
    val user = User()
    user.sayHi() // ? 正常呼叫,IDE 不會報錯,但執行時會出錯
    
    複製程式碼

    為什麼這裡不會報錯呢?因為 Kotlin 中的異常是不會被檢查的,只有在執行時如果 sayHi 丟擲異常,才會出錯。

  • Kotlin 中 try-catch 語句也可以是一個表示式,允許程式碼塊的最後一行作為返回值:

    ?️
               ?       
    val a: Int? = try { parseInt(input) } catch (e: NumberFormatException) { null }
    
    複製程式碼

?.?:

我們在之前的文章中已經講過 Kotlin 的空安全,其實還有另外一個常用的複合符號可以讓你在判空時更加方便,那就是 Elvis 操作符 ?:

我們知道空安全呼叫 ?.,在物件非空時會執行後面的呼叫,物件為空時就會返回 null。如果這時將該表示式賦值給一個不可空的變數:

?️
val str: String? = "Hello"
var length: Int = str?.length
//                ? ,IDE 報錯,Type mismatch. Required:Int. Found:Int?

複製程式碼

報錯的原因就是 str 為 null 時我們沒有值可以返回給 length

這時就可以使用 Kotlin 中的 Elvis 操作符 ?: 來兜底:

?️
val str: String? = "Hello"
                             ?
val length: Int = str?.length ?: -1

複製程式碼

它的意思是如果左側表示式 str?.length 結果為空,則返回右側的值 -1

Elvis 操作符還有另外一種常見用法,如下:

?️
fun validate(user: User) {
    val id = user.id ?: return // ? 驗證 user.id 是否為空,為空時 return 
}

// 等同於

fun validate(user: User) {
    if (user.id == null) {
        return
    }
    val id = user.id
}

複製程式碼

看到這裡,想必你對 Kotlin 的空安全有了更深入的瞭解了,下面我們再看看 Kotlin 的相等比較符。

=====

我們知道在 Java 中,== 比較的如果是基本資料型別則判斷值是否相等,如果比較的是 String 則表示引用地址是否相等, String 字串的內容比較使用的是 equals()

☕️
String str1 = "123", str2 = "123";
System.out.println(str1.equals(str2));
System.out.println(str1 == str2); 

複製程式碼

Kotlin 中也有兩種相等比較方式:

  • == :可以對基本資料型別以及 String 等型別進行內容比較,相當於 Java 中的 equals
  • === :對引用的記憶體地址進行比較,相當於 Java 中的 ==

可以發現,Java 中的 equals ,在 Kotlin 中與之相對應的是 ==,這樣可以使我們的程式碼更加簡潔。

下面再來看看程式碼示例:

?️
val str1 = "123"
val str2 = "123"
println(str1 == str2) // ? 內容相等,輸出:true

val str1= "字串"
val str2 = str1
val str3 = str1
print(str2 === str3) // ? 引用地址相等,輸出:true

複製程式碼

其實 Kotlin 中的 equals 函式是 == 的操作符過載,關於操作符過載,這裡先不講,之後的文章會講到。

練習題

  1. 請按照以下要求實現一個 Student 類:
    • 寫出三個構造器,其中一個必須是主構造器
    • 主構造器中的引數作為屬性
    • 寫一個普通函式 show,要求通過字串模板輸出類中的屬性
  2. 編寫程式,使用今天所講的操作符,找出集合 {21, 40, 11, 33, 78} 中能夠被 3 整除的所有元素,並輸出。

作者介紹

視訊作者

扔物線(朱凱)
  • 碼上開學創始人、專案管理人、內容模組規劃者和視訊內容作者。
  • Android GDE( Google 認證 Android 開發專家),前 Flipboard Android 工程師。
  • GitHub 全球 Java 排名第 92 位,在 GitHub 上有 6.6k followers 和 9.9k stars。
  • 個人的 Android 開源庫 MaterialEditText 被全球多個專案引用,其中包括在全球擁有 5 億使用者的新聞閱讀軟體 Flipboard 。
  • 曾多次在 Google Developer Group Beijing 線下分享會中擔任 Android 部分的講師。
  • 個人技術文章《給 Android 開發者的 RxJava 詳解》釋出後,在國內多個公司和團隊內部被轉發分享和作為團隊技術會議的主要資料來源,以及逆向傳播到了美國一些如 Google 、 Uber 等公司的部分華人團隊。
  • 創辦的 Android 高階進階教學網站 HenCoder 在全球華人 Android 開發社群享有相當的影響力。
  • 之後創辦 Android 高階開發教學課程 HenCoder Plus ,學員遍佈全球,有來自阿里、頭條、華為、騰訊等知名一線網際網路公司,也有來自台灣、日本、美國等地區的資深軟體工程師。

文章作者

Sinyu(沈新宇)

Sinyu(沈新宇) ,即刻 Android 工程師。2019 年加入即刻,參與即刻 6.0 的產品迭代,以及負責中臺基礎建設。獨立開發並運營過一款使用者過萬的 App。

相關文章