學習 Kotlin 程式設計語言

Kotlin 是世界各地的 Android 開發人員廣泛使用的程式設計語言。本主題為 Kotlin 速成課程,可助您快速上手。

變數宣告

Kotlin 使用兩個不同關鍵字來宣告變數:valvar

  • 使用 val 做為值永不改變的變數。您無法將值重新指派給使用 val 宣告的變數。
  • 使用 var 做為值可變的變數。

在以下範例中,countInt 類型的變數,被指派的初始值為 10

var count: Int = 10

Int 是代表整數的類型,即可在 Kotlin 中呈現的眾多數值類型之一。與其他語言類似,您也可以依據數值型資料而使用 ByteShortLongFloatDouble

var 關鍵字用於視需要向 count 重新指派值。例如,您可以將 count 的值從 10 變更為 15

var count: Int = 10
count = 15

不過,有些值不能變更。假設有一個 String,稱為 languageName。如要確保 languageName 始終維持「Kotlin」的值,請使用 val 關鍵字宣告 languageName

val languageName: String = "Kotlin"

這些關鍵字可讓您清楚知道,哪些項目可以變更。您可以視需要善加利用。如果必須重新指派變數的參照,請將其宣告為 var。否則,請使用 val

類型推論

接著上面的範例,當您將初始值指派給 languageName 時,Kotlin 編譯器可根據指派值的類型來推斷類型。

由於 "Kotlin" 的值是 String 類型,編譯器會推斷 languageName 也是 String。請注意,Kotlin 是一種「靜態」語言。這意味著,該類型已在編譯時間經過解析,且不會有任何變化。

在以下範例中,系統會將 languageName 推斷為 String,因此您無法呼叫任何不屬於 String 類別的函式:

val languageName = "Kotlin"
val upperCaseName = languageName.toUpperCase()

// Fails to compile
languageName.inc()

toUpperCase() 是只能在 String 類型的變數上呼叫的函式。由於 Kotlin 編譯器將 languageName 推斷為 String,您可以放心呼叫 toUpperCase()。不過,inc()Int 運算子函式,所以無法在 String 上呼叫該函式。Kotlin 得出類型推論的方式,同時兼顧了簡潔性和類型安全。

空值安全

在某些語言中,您可以宣告參照類型變數,而不指派明確的初始值。在這些情況下,變數通常包含空值。根據預設,Kotlin 變數無法保留空值。這意味著,以下程式碼片段無效:

// Fails to compile
val languageName: String = null

變數必須為「可為空值」類型,才能保留空值。您可以在變數類型後面加上 ?,藉此將變數指定為可為空值,如以下範例所示:

val languageName: String? = null

透過 String? 類型,您可以將 String 值或 null 指派給 languageName

您必須小心處理可為空值的變數,否則會產生可怕的 NullPointerException。例如,在 Java 中,如果嘗試對空值叫用方法,程式就會當機。

Kotlin 提供多項機制,可以安全地使用可為空值的變數。詳情請參閱 Android 中常見的 Kotlin 模式:是否可為空值一節。

條件式

Kotlin 使用幾種機制來實作條件式邏輯。最常見的機制是「if-else 陳述式」。如果 if 關鍵字後面括號內運算式的計算結果是 true,則會執行該分支版本中的程式碼 (即緊接在後方大括號內的程式碼)。否則,系統會執行 else 分支版本中的程式碼。

if (count == 42) {
    println("I have the answer.")
} else {
    println("The answer eludes me.")
}

您可以使用 else if 來表示多個條件式。該陳述式單一條件陳述式中表示更精細、更複雜的邏輯,如以下範例所示:

if (count == 42) {
    println("I have the answer.")
} else if (count > 35) {
    println("The answer is close.")
} else {
    println("The answer eludes me.")
}

條件陳述式可用於表示有狀態的邏輯,但您可能會發現,寫入這些陳述式時邏輯會重複。在上述範例中,您只要在每個分支版本中列印 String 即可。為了避免重複,Kotlin 提供了「條件運算式」。最後一個範例的改寫方式如下:

val answerString: String = if (count == 42) {
    "I have the answer."
} else if (count > 35) {
    "The answer is close."
} else {
    "The answer eludes me."
}

println(answerString)

毫無疑問,每個條件分支版本會傳回在最後一行上的運算式的結果,因此無需使用 return 關鍵字。由於所有三個分支版本得出的結果均為 String 類型,所以 if-else 運算式的結果也是 String 類型。在本例中,answerString 的初始值是從 if-else 運算式的結果指派而來。類型推論可用於省略 answerString 的明確類型宣告,但為清楚起見,建議加入這個宣告。

隨著條件陳述式的複雜程度增加,您可以考慮將 If-else 運算式替換為 「when」 運算式,如以下範例所示:

val answerString = when {
    count == 42 -> "I have the answer."
    count > 35 -> "The answer is close."
    else -> "The answer eludes me."
}

println(answerString)

when 運算式中的每個分支版本都會以條件、箭頭 (->) 和結果表示。如果箭頭左側條件的計算結果為評估為「是」,則傳回右側運算式的結果。請注意,執行作業不會從一個分支版本移至下一個分支版本。when 運算式範例中的程式碼與前一個範例的程式碼等效,但按理來說更容易讀取。

Kotlin 的條件式側重於其中一種更為強大的功能,即「智慧型層級轉換」。您可以使用條件陳述式檢查變數是否包含空值的參照,而不是使用安全呼叫運算子或非空值斷言運算子來處理可為空值的值,如以下範例所示:

val languageName: String? = null
if (languageName != null) {
    // No need to write languageName?.toUpperCase()
    println(languageName.toUpperCase())
}

在條件分支版本中,languageName 可視為不可為空值。Kotlin 非常聰明,可識別執行分支版本的條件,也就是讓 languageName 不保留空值,因此您不必在這個分支版本中將 languageName 視為可為空值。這個智慧型層級轉換功能可用於空值檢查、類型檢查,或任何符合合約條件的條件。

函式

您可以將一或多個運算式分入一個「函式」。您可以將運算式納入函式中,並呼叫該函式,而不是在每次需要結果時重複執行同一系列運算式。

如要宣告函式,請使用 fun 關鍵字,後接函式名稱。接下來,請定義函式接收的輸入內容類型 (如果有),並宣告該函式傳回的輸出內容類型。函式主體用於定義叫用函式時呼叫的運算式。

以下是根據前幾個範例建構的完整 Kotlin 函式:

fun generateAnswerString(): String {
    val answerString = if (count == 42) {
        "I have the answer."
    } else {
        "The answer eludes me"
    }

    return answerString
}

上述範例中的函式名稱為 generateAnswerString。該函式不會接收任何輸入內容,反而會輸出 String 類型的結果。如要呼叫函式,請使用其名稱,後面加上叫用運算子 (())。在以下範例中,answerString 變數是以 generateAnswerString() 的結果完成初始化調整。

val answerString = generateAnswerString()

函式可將引數視為輸入內容,如以下範例所示:

fun generateAnswerString(countThreshold: Int): String {
    val answerString = if (count > countThreshold) {
        "I have the answer."
    } else {
        "The answer eludes me."
    }

    return answerString
}

宣告函式時,您可以指定任意數量的引數及其類型。在上述範例中,generateAnswerString() 會接收名為 countThreshold、具有 Int 類型的引數。在此函式內,您可以使用函式的名稱參照該引數。

呼叫此函式時,您必須在函式呼叫的括號中加入引數:

val answerString = generateAnswerString(42)

簡化函式宣告

generateAnswerString() 是一個相當簡單的函式。此函式會宣告變數,然後立即傳回結果。從函式傳回單一運算式的結果時,您可以直接傳回函式中 if-else 運算式的結果,略過宣告本機變數,如以下範例所示:

fun generateAnswerString(countThreshold: Int): String {
    return if (count > countThreshold) {
        "I have the answer."
    } else {
        "The answer eludes me."
    }
}

您也可以將回傳結果替換為指派運算子:

fun generateAnswerString(countThreshold: Int): String = if (count > countThreshold) {
        "I have the answer"
    } else {
        "The answer eludes me"
    }

匿名函式

並非所有函式都需要名稱。某些函式可透過輸入內容和輸出內容更直接地識別。這些函式稱為「匿名函式」。您可以保留匿名函式的參照,以便日後使用這個參照來呼叫匿名函式。就像其他參照類型一樣,您也可以傳遞與應用程式相關的參照。

val stringLengthFunc: (String) -> Int = { input ->
    input.length
}

如同已命名的函式,匿名函式也可以包含任意數量的運算式。函式傳回的值是最後一個運算式的結果。

在上述範例中,stringLengthFunc 包含匿名函式的參照。該函式將 String 視為輸入內容,並傳回輸入內容 String 的長度,來做為 Int 類型的輸出內容。因此,函式的類型以 (String) -> Int 表示。不過,此程式碼不會叫用函式。如要擷取函式的結果,您必須依據已命名函式的做法來叫用函式。您必須在呼叫 stringLengthFunc 時提供 String,如以下範例所示:

val stringLengthFunc: (String) -> Int = { input ->
    input.length
}

val stringLength: Int = stringLengthFunc("Android")

高階函式

一個函式將另一個函式視為引數。使用其他函式做為引數的函式稱為「高階函式」。這種模式可用於元件之間的通訊,方式與在 Java 中使用回呼介面一樣。

以下是高階函式的範例:

fun stringMapper(str: String, mapper: (String) -> Int): Int {
    // Invoke function
    return mapper(str)
}

stringMapper() 函式會接收 String 以及一個函式。後一個函式會從您傳入的 String 衍生 Int 值。

如要呼叫 stringMapper(),您可以傳遞 String 和符合另一個輸入內容參數條件的函式,也就是將 String 視為輸入內容並輸出 Int 的函式,如以下範例所示:

stringMapper("Android", { input ->
    input.length
})

如果匿名函式是某個函式上定義的「最後一個」參數,您可以將匿名函式傳遞至叫用後一個函式的括號外,如以下範例所示:

stringMapper("Android") { input ->
    input.length
}

您可以在 Kotlin 標準程式庫中輕易找到匿名函式。詳情請參閱「高階函式和 Lambda」一文。

類別

目前提及的所有類型都在 Kotlin 程式設計語言內建構。如果您想新增自訂類型,可以使用 class 關鍵字定義類別,如以下範例所示:

class Car

屬性

類別使用屬性來表示狀態。屬性是類別層級的變數,可包含 getter、setter 和支援欄位。由於汽車要有輪子才能行駛,您可以將 Wheel 物件清單新增為 Car 的屬性,如以下範例所示:

class Car {
    val wheels = listOf<Wheel>()
}

請注意,wheelspublic val,意味著 wheels 可透過 Car 類別以外的位置存取,但無法重新指派。如要取得 Car 的執行個體,您必須先呼叫其建構函式。在這裡,您可以存取它的任何可存取屬性。

val car = Car() // construct a Car
val wheels = car.wheels // retrieve the wheels value from the Car

如要自訂輪子,您可以定義自訂的建構函式,用來指定類別屬性的初始化方式:

class Car(val wheels: List<Wheel>)

在上述範例中,類別建構函式將 List<Wheel> 視為建構函式引數,並使用該引數對其 wheels 屬性進行初始化調整。

類別函式與封裝

類別會使用函式來模擬行為。函式可以修改狀態,藉此只顯示要公開的資料。這個存取權控管是大型物件導向概念的一部分,稱為「封裝」

在以下範例中,doorLock 屬性不會對 Car 類別以外的任何類別公開。如要解鎖車輛,您必須呼叫傳入有效金鑰的 unlockDoor() 函式,如以下範例所示:

class Car(val wheels: List<Wheel>) {

    private val doorLock: DoorLock = ...

    fun unlockDoor(key: Key): Boolean {
        // Return true if key is valid for door lock, false otherwise
    }
}

如要自訂屬性的參照方式,您可以提供自訂的 getter 和 setter。例如,如果您想公開屬性的 getter,同時限制對 setter 的存取權,您可以將該 setter 指定為 private

class Car(val wheels: List<Wheel>) {

    private val doorLock: DoorLock = ...

    var gallonsOfFuelInTank: Int = 15
        private set

    fun unlockDoor(key: Key): Boolean {
        // Return true if key is valid for door lock, false otherwise
    }
}

您可以結合使用屬性和函式,來建立模擬所有物件類型的類別。

互通性

Kotlin 最重要的功能之一,就是與 Java 流暢的互通性。由於 Kotlin 程式碼可編譯成 JVM 位元碼,所以您的 Kotlin 程式碼可以直接呼叫 Java 程式碼,反之亦然。這意味著,您可以直接從 Kotlin 使用現有 Java 程式庫。此外,大部分 Android API 均以 Java 編寫,所以您可以直接從 Kotlin 呼叫這些 API。

後續步驟

Kotlin 是一種靈活、實用的語言,所受支援日益增加,因而發展迅猛。如果您沒用過這種語言,建議您試一下。如要瞭解後續步驟,請參閱官方 Kotlin 說明文件,以及如何在 Android 應用程式中套用常見的 Kotlin 模式指南。