泛型、物件和擴充功能

1. 簡介

在過去數十年間,程式設計人員開發出了多種程式設計語言功能,為的就是改善程式碼的撰寫品質,例如用更少的程式碼表達相同的概念、用抽象化表達複雜的概念,以及在撰寫程式碼的時候想辦法防止其他開發人員發生預料之外的錯誤等等。Kotlin 語言也是如此,很多功能就是為了協助開發人員撰寫出更能清楚說明概念的程式碼。

不過,如果您剛開始接觸程式設計,可能會覺得這些功能非常難用。雖然這些功能聽起來很實用,但是不一定人人都能瞭解究竟多實用,又能解決哪些問題。您可能已經看過 Compose 及其他程式庫的部分功能。

雖然實際經驗依然無法替代,但是這篇程式碼研究室將會為您介紹幾個 Kotlin 概念,以便協助您構築更大型的應用程式:

  • 泛型
  • 不同種類的類別 (列舉類別和資料類別)
  • 單例模式物件和伴生物件
  • 擴充功能屬性和函式
  • 範圍函式

完成這篇程式碼研究室之後,您對本課程舉出的程式碼應該會有更進一步的認識,並會看到幾個在撰寫應用程式時會碰到或用到這些概念的範例。

必要條件

  • 熟悉物件導向程式設計概念,包括繼承。
  • 如何定義和實作介面。

課程內容

  • 如何為類別定義泛型參數。
  • 如何執行個體化泛型類別。
  • 使用列舉和資料類別的時機。
  • 如何定義必須實作介面的泛型參數。
  • 如何使用範圍函式存取類別屬性和方法。
  • 如何為類別定義單例模式物件和伴生物件。
  • 如何運用新屬性和方法擴充類別。

軟硬體需求

  • 可存取 Kotlin Playground 的網路瀏覽器。

2. 運用泛型製作可重複使用的類別

假設您要撰寫一個網路測驗的應用程式,內容和您在本篇課程看到的測驗相似。測驗通常會有多種題型,例如填充題或是非題。每個測驗題目都可以用一個類別代表,其中設有多種屬性。

測驗題目的文字可以用字串代表。測驗題目也需要可以代表答案。不過,不同的題型 (例如是非題) 可能需要用不同的資料類型代表答案。我們在此定義三種問題類型。

  • 填充題:答案是一個字詞,以 String 代表。
  • 是非題:答案以 Boolean 代表。
  • 計算題:答案是一組數值。簡單的算數題答案會用 Int 代表。

另外,我們的範例題目不管是哪種題型都會說明難度分級。難度分級以字串代表,並可能有三種值:"easy""medium""hard"

定義類別,藉此代表每種題型:

  1. 前往 Kotlin playground
  2. main() 函式上方定義填充題的類別,並命名為 FillInTheBlankQuestion,其中有 questionTextString 屬性、answerString 屬性,以及 difficultyString 屬性。
class FillInTheBlankQuestion(
    val questionText: String,
    val answer: String,
    val difficulty: String
)
  1. FillInTheBlankQuestion 類別下方定義另一個名為 TrueOrFalseQuestion 的類別用於是非題,並包含 questionTextString 屬性、answerBoolean 屬性,以及 difficultyString 屬性。
class TrueOrFalseQuestion(
    val questionText: String,
    val answer: Boolean,
    val difficulty: String
)
  1. 最後,在其他兩個類別下方定義 NumericQuestion 類別,其中有 questionTextString 屬性、answerInt 屬性,以及 difficultyString 屬性。
class NumericQuestion(
    val questionText: String,
    val answer: Int,
    val difficulty: String
)
  1. 請看您撰寫的程式碼。有發現重複的內容嗎?
class FillInTheBlankQuestion(
    val questionText: String,
    val answer: String,
    val difficulty: String
)

class TrueOrFalseQuestion(
    val questionText: String,
    val answer: Boolean,
    val difficulty: String
)
class NumericQuestion(
    val questionText: String,
    val answer: Int,
    val difficulty: String
)

以下三個類別都有相同的屬性:questionTextanswerdifficulty。唯一的不同之處只有 answer 屬性的資料類型。您可能會認為最簡單的解決方式就是為 questionTextdifficulty 建立父項類別,然後用各個子類別定義 answer 屬性。

不過,使用繼承會產生跟以上相同的問題。每次新增題型時,都必須新增 answer 屬性。唯一的不同之處只有資料類型。而且父項類別 Question 沒有答案屬性,看起來也非常突兀。

如果您想讓屬性有不同的資料類型,則設定子類別並不適合。Kotlin 有稱作「泛型」的功能,可讓單一屬性根據特定用途產生不同的資料類型。

什麼是泛型資料類型?

泛型類型簡稱「泛型」,可允許類別等資料類型指定未知的預留位置資料類型做為屬性和方法。這代表什麼意思?

在以上範例中,您可以建立一個代表任何問題的類別,然後使用預留位置名稱做為 answer 屬性的資料類型,不用為每種可能需要使用的資料類型來定義答案屬性。當該類別執行個體化的時候,才會指定實際的資料類型:StringIntBoolean 等等。每次使用這個預留位置名稱時,系統便會改用傳遞到該類別的資料類型。定義泛型做為類別的語法如下:

67367d9308c171da.png

將某類別例項化時,系統會提供泛型資料類型,因此需要將泛型資料類型定義為類別簽章的一部分。類別名稱後方會接上一個左角括號 (<),然後是該資料類型的預留位置名稱,再後面則是一個右角括號 (>)。

每當需要在類別內使用真實的資料類型時,就可以使用這個預留位置名稱,例如屬性就能這樣使用。

81170899b2ca0dc9.png

這和其他屬性宣告方式相同,只是不使用資料類型,而使用預留位置名稱。

類別最後該怎麼知道要使用哪種資料類型?您將該類別例項化時,泛型所使用的資料類型會傳遞到角括號內,當做參數。

9b8fce54cac8d1ea.png

類別名稱後方會接上一個左角括號 (<),然後是實際的資料類型 (StringBooleanInt 等),再後面則是一個右角括號 (>)。傳遞給泛型屬性的值資料類型,必須和角括號內的資料類型相符。之所以要把答案屬性設為泛型,是可以藉此用一個類別代表所有測驗題型,無論答案是 StringBooleanInt 還是任何資料類型都可以套用。

重構程式碼以便使用泛型

重構程式碼並使用單一個設有泛型答案屬性的類別,並命名為 Question

  1. 移除 FillInTheBlankQuestionTrueOrFalseQuestionNumericQuestion 的類別定義。
  2. 建立新的類別,命名為 Question
class Question()
  1. 在類別名稱後、括號之前,用左右角括號加入泛型類型參數。呼叫泛型類型 T
class Question<T>()
  1. 新增 questionTextanswerdifficulty 屬性。questionText 應該是 String 類型。由於 answer 會在執行個體化 Question 類別的時候指定資料類型,所以應該是 T 類型。difficulty 屬性應該是 String 類型。
class Question<T>(
    val questionText: String,
    val answer: T,
    val difficulty: String
)
  1. 如果想看這些內容如何和多種題型 (填充題、是非題等等) 互動,請在 main() 內建立三個 Question 的執行個體,如下所示。
fun main() {
    val question1 = Question<String>("Quoth the raven ___", "nevermore", "medium")
    val question2 = Question<Boolean>("The sky is green. True or false", false, "easy")
    val question3 = Question<Int>("How many days are there between full moons?", 28, "hard")
}
  1. 執行程式碼,確認一切正常。現在應該會有三個 Question 類別的執行個體,每個類別都有不同的答案資料類型,而不是三個不同的類別,也並非使用繼承。如果想處理使用不同答案類型的問題,則可以重複利用同一個 Question 類別。

3. 使用列舉類別

在前一段章節中,您已經用三種可能出現的值定義了難度屬性:「簡單」、「中等」和「困難」。雖然這樣也可以正常運作,但是仍然有幾個問題。

  1. 如果不小心把其中一種可能出現的字串打錯,就可能會發生錯誤。
  2. 如果值有所變動,例如將 "medium" 重新命名為 "average",那麼所有用到這個字串的地方都需要更新。
  3. 您或其他開發人員隨時都有可能不小心用到其他並非這三種有效值的字串。
  4. 如果新增更多難度等級,程式碼也會更難維護。

Kotlin 可以協助您解決這些問題,只要使用名為「列舉類別」的特殊類別即可。透過使用列舉類別,就能建立內含有限數量可能值的類型。舉例來說,現實生活的方位 (東、南、西、北) 就能用列舉類別代表。您不需要使用其他方向,而程式碼也不允許使用其他方向。列舉類別語法如下所示。

f4bddb215eb52392.png

列舉的每項可能值都稱做「列舉常數」。列舉常數位於大括號內,並以半形逗號分隔。慣例上,常數名稱所有字母都會使用大寫。

您會使用點號運算子參照列舉常數。

f3cfa84c3f34392b.png

使用列舉常數

修改程式碼並使用列舉嘗試代表難度,而不是 String

  1. Question 類別下方定義 enum 類別,命名為 Difficulty
enum class Difficulty {
    EASY, MEDIUM, HARD
}
  1. Question 類別中 difficulty 屬性的資料類型從 String 變更為 Difficulty
class Question<T>(
    val questionText: String,
    val answer: T,
    val difficulty: Difficulty
)
  1. 在初始化三種問題的時候,傳遞列舉常數當做難度。
val question1 = Question<String>("Quoth the raven ___", "nevermore", Difficulty.MEDIUM)
val question2 = Question<Boolean>("The sky is green. True or false", false, Difficulty.EASY)
val question3 = Question<Int>("How many days are there between full moons?", 28, Difficulty.HARD)

4. 使用資料類別

您到目前為止已經用過很多種類別 (例如 Activity 的子類別),而這些類別有幾種可以執行不同操作的方法。這些類別不只代表資料,也含有多種功能。

不過,像是 Question 這種類別就只含有資料。並未含有可以執行操作的方法。這些類別可以定義為「資料類別」。把類別定義為資料類別,就能讓 Kotlin 編譯器做出特定假設,並自動實作某些方法。以 println() 功能在背景呼叫 toString() 為例。如果您使用資料類別的話,系統可以自動根據該類別的屬性實作 toString() 和其他方法。

如果想定義資料類別,只要在 class 關鍵字前方加入 data 關鍵字即可。

e7cd946b4ad216f4.png

Question 轉換為資料類別

首先,您會看到在非資料類別呼叫 toString() 這種方法時會發生什麼事。然後將 Question 轉換為資料類別,讓系統預設實作此方法和其他方法。

  1. main() 內列印在 question1 呼叫 toString() 的結果。
fun main() {
    val question1 = Question<String>("Quoth the raven ___", "nevermore", Difficulty.MEDIUM)
    val question2 = Question<Boolean>("The sky is green. True or false", false, Difficulty.EASY)
    val question3 = Question<Int>("How many days are there between full moons?", 28, Difficulty.HARD)
    println(question1.toString())
}
  1. 執行程式碼。輸出內容只有顯示物件的類別名稱和專屬 ID。
Question@37f8bb67
  1. 使用 data 關鍵字將 Question 轉換為資料類別。
data class Question<T>(
    val questionText: String,
    val answer: T,
    val difficulty: Difficulty
)
  1. 再次執行程式碼。標記為資料類別後,Kotlin 就能在呼叫 toString() 時判斷如何顯示類別的屬性。
Question(questionText=Quoth the raven ___, answer=nevermore, difficulty=MEDIUM)

如果有類別定義為資料類別,便會實作以下方法。

  • equals()
  • hashCode():使用特定集合類型時就可以看到這項方法。
  • toString()
  • componentN()component1()component2() 等等。
  • copy()

5. 使用單例模式物件

在很多情況下,會需要讓類別裡只有一個執行個體。例如:

  1. 手機遊戲目前使用者的玩家資料。
  2. 和單一硬體裝置互動時,例如將音訊傳送到音響。
  3. 用來存取遠端資料來源 (例如 Firebase 資料庫) 的物件。
  4. 進行驗證,一次應該只能登入一名使用者。

在以上情況中,您大概都需要使用類別。不過,這些類別都只需要執行個體化單一執行個體。如果只有一個硬體裝置,或是一次只能登入一名使用者,那麼實在不需要建立超過一個執行個體。如果同時有兩個物件存取同一個硬體裝置,就可能發生怪異且容易出問題的行為。

您可以把物件定義為單例模式,在程式碼裡清楚說明該物件應該只能有一個執行個體。「單例模式」是一種只能有單一執行個體的類別。Kotlin 可以提供特殊的建構 (稱為「物件」),而您可以用來建立單例模式類別。

定義單例模式物件

645e8e8bbffbb5f9.png

物件語法和類別語法很相似。只要使用 object 關鍵字取代 class 關鍵字即可。由於您無法直接建立執行個體,因此單例模式物件不能含有建構函式。而是在大括弧裡面定義所有屬性,並給予一個初始值。

我們之前展示的部分範例對您來說可能不太明顯,而如果您從來沒有需要配合特定硬體裝置或在應用程式內處理驗證作業的話就更難發現這一點。不過,如果您繼續學習 Android 開發方法,就會看到越來越多種單例模式物件。我們可以用一個簡單的範例展示實際運作情況,此範例中以物件代表使用者狀態,並只需要一個執行個體。

測驗最好能夠記錄所有問題的數量,以及學生已經作答過的問題數量。這個類別只需要有一個執行個體,所以不需要宣告為類別,而是宣告為單例模式物件。

  1. 建立名為 StudentProgress 的物件。
object StudentProgress {
}
  1. 在本範例內,我們假設總共有十個題目,其中三題已經作答完畢。新增兩個 Int 屬性:total 使用 10 的值,而 answered 使用 3 的值。
object StudentProgress {
    var total: Int = 10
    var answered: Int = 3
}

存取單例模式物件

您還記得不能直接為單例模式物件建立執行個體嗎?那麼該如何存取這種物件的屬性呢?

由於 StudentProgress 一次只有一個例項,因此您參照該物件本身的名稱,並在後方加上點號運算子 (.) 和屬性名稱後,即可存取屬性。

1b610fd87e99fe25.png

更新 main() 函式,以便存取單例模式物件的屬性。

  1. main() 加入 println() 的呼叫,並讓它從 StudentProgress 物件輸出 answeredtotal 的問題。
fun main() {
    ...
    println("${StudentProgress.answered} of ${StudentProgress.total} answered.")
}
  1. 執行程式碼,確認運作正常。
...
3 of 10 answered.

將物件宣告為伴生物件

Kotlin 的類別和物件可以在其他類型當中定義,非常適合用來整理程式碼。您可以使用「伴生物件」在其他類別裡面定義單例模式物件。如果伴生物件的屬性和方法屬於某類別,您可以從類別內存取這些屬性和方法,寫出更簡潔的語法。

如要宣告伴生物件,只要在 object 關鍵字前方加上 companion 關鍵字即可。

68b263904ec55f29.png

您將會建立名為 Quiz 的新類別,用於儲存測驗題目,然後將 StudentProgress 設為 Quiz 類別的伴生物件。

  1. Difficulty 列舉下方定義新的類別,並命名為 Quiz
class Quiz {
}
  1. question1question2question3main() 移到 Quiz 類別。如果您還沒移除 println(question1.toString()) 的話,也需要加以移除。
class Quiz {
    val question1 = Question<String>("Quoth the raven ___", "nevermore", Difficulty.MEDIUM)
    val question2 = Question<Boolean>("The sky is green. True or false", false, Difficulty.EASY)
    val question3 = Question<Int>("How many days are there between full moons?", 28, Difficulty.HARD)

}
  1. StudentProgress 物件移至 Quiz 類別中。
class Quiz {
    val question1 = Question<String>("Quoth the raven ___", "nevermore", Difficulty.MEDIUM)
    val question2 = Question<Boolean>("The sky is green. True or false", false, Difficulty.EASY)
    val question3 = Question<Int>("How many days are there between full moons?", 28, Difficulty.HARD)

    object StudentProgress {
        var total: Int = 10
        var answered: Int = 3
    }
}
  1. StudentProgress 物件標記 companion 關鍵字。
companion object StudentProgress {
    var total: Int = 10
    var answered: Int = 3
}
  1. 把呼叫更新為 println(),參照 Quiz.answeredQuiz.total 的屬性。雖然這些屬性已經在 StudentProgress 物件裡面宣告,依然可以透過只用 Quiz 類別名稱的點號標記法進行存取。
fun main() {
    println("${Quiz.answered} of ${Quiz.total} answered.")
}
  1. 執行程式碼以驗證輸出內容。
3 of 10 answered.

6. 運用新屬性和方法擴充類別

當您使用 Compose 時,可能曾在指定 UI 元素大小時看過一些特別不同的語法。您會看到 Double 等數字類型使用 dpsp 這些屬性來指定尺寸。

a25c5a0d7bb92b60.png

為什麼 Kotlin 語言程式設計師會特別在建構 Android UI 時,在內建的資料類型裡設定屬性和函式呢?難道他們可以預知未來嗎?難道在 Compose 問世以前,Kotlin 就決定要跟 Compose 搭配使用了嗎?

當然不是了!撰寫類別時,通常無法確定其他應用程式開發人員會怎麼使用 (或打算怎麼使用) 這個類別,因此不可能預測日後所有的類別用途,而且為了無法預測的用途無端擴充程式碼,也並非明智之舉。

Kotlin 語言的做法是讓其他開發人員得以擴充現有的資料類型,加入可以用點號語法存取的屬性和方法,把這些內容也當做屬於該資料類型。如果是未在 Kotlin 中處理過浮點類型的開發人員 (例如 Compose 程式庫建構人員),可能會選擇新增 UI 尺寸專用的屬性和方法。

既然您已經在 Compose 教學的最開始兩個單元內看過這項語法,現在就該是您瞭解背後運作原理的時候了。您將會加入一些屬性和方法,藉此擴充現有的類型。

新增擴充功能屬性

如果想定義擴充功能屬性,請在變數名稱前面加上類型名稱和點號運算子 (.)。

1e8a52e327fe3f45.png

您將重構 main() 函式中的程式碼,顯示含有擴充功能屬性的測驗進度。

  1. Quiz 類別下方定義 Quiz.StudentProgress 的擴充功能屬性,並命名為 String 類型的 progressText
val Quiz.StudentProgress.progressText: String
  1. 為這個擴充功能屬性定義 getter,讓它在 main() 中傳回之前使用的相同字串。
val Quiz.StudentProgress.progressText: String
    get() = "${answered} of ${total} answered"
  1. main() 函式中的程式碼替換為顯示 progressText 的程式碼。這是伴生物件的擴充功能屬性,因此使用點號標記法就能以該類別的名稱 Quiz 存取此屬性。
fun main() {
    println(Quiz.progressText)
}
  1. 執行程式碼,確定可以正常運作。
3 of 10 answered.

新增擴充功能函式

如果想定義擴充功能函式,請在函式名稱前面加上類型名稱和點號運算子 (.)。

879ff2761e04edd9.png

您需要加入擴充功能函式,才能將測驗進度輸出為進度列。由於您無法實際在 Kotlin playground 製作進度列,所以需要用文字列印懷舊風格的進度列!

  1. StudentProgress 物件新增擴充功能函式,命名為 printProgressBar()。這個函式應該不含任何參數,也不會回傳值。
fun Quiz.StudentProgress.printProgressBar() {
}
  1. 使用 repeat() 列印 字元和 answered 的次數。進度列的這個暗色部分代表已經回答的題數。因為每個字元後面不需要新的行,因此要使用 print()
fun Quiz.StudentProgress.printProgressBar() {
    repeat(Quiz.answered) { print("▓") }
}
  1. 使用 repeat() 列印 字元和等於 totalanswered 之間差距的次數。進度列的這個淺色部分代表剩下的題目。
fun Quiz.StudentProgress.printProgressBar() {
    repeat(Quiz.answered) { print("▓") }
    repeat(Quiz.total - Quiz.answered) { print("▒") }
}
  1. 用沒有引數的 println() 列印新的行,然後再列印 progressText
fun Quiz.StudentProgress.printProgressBar() {
    repeat(Quiz.answered) { print("▓") }
    repeat(Quiz.total - Quiz.answered) { print("▒") }
    println()
    println(Quiz.progressText)
}
  1. 更新 main() 裡的程式碼,使其呼叫 printProgressBar()
fun main() {
    Quiz.printProgressBar()
}
  1. 執行程式碼以驗證輸出內容。
▓▓▓▒▒▒▒▒▒▒
3 of 10 answered.

一定要進行這些步驟嗎?其實並不一定。不過,有了擴充功能屬性和方法後,您就能以更多種方式向其他開發人員展示程式碼。在其他類型使用點號語法,可以幫助您自己和其他開發人員更容易判讀程式碼內容。

7. 運用介面重新編寫擴充功能函式

在上一頁中,您已瞭解如何使用擴充功能屬性和擴充功能函式,將屬性和方法新增至 StudentProgress 物件,而不必直接在物件中新增程式碼。雖然這種方法很適合用來為已定義的單一類別添加功能,但如果您可以存取原始碼,就不必擴充類別。在某些情況下,您可能並不清楚實作方式,只知道應有特定方法或屬性。如果您需要多個類別皆包含相同的額外屬性和方法 (但運作方式可能不同),可以使用「介面」定義這些屬性和方法。

舉例來說,除了測驗外,假設您還有問卷調查課程、食譜步驟,或者可能會使用進度列的任何其他排序資料。您可以定義一個介面,用於指定每個類別必須包含的方法及/或屬性。

eeed58ed687897be.png

使用 interface 關鍵字,後接 UpperCamelCase 格式的名稱和左右大括號,即可定義介面。在大括號內,您可以定義任何方法簽章或 get-only 屬性,且所有符合介面的類別都必須實作這些簽章或屬性。

6b04a8f50b11f2eb.png

介面是一項合約。類別與介面相符即可擴充介面。類別可宣告其本身需要使用冒號 (:) 後接空格及介面名稱,才能擴充介面。

78af59840c74fa08.png

為此,類別必須實作介面中指定的所有屬性和方法。這樣一來,您就能輕鬆確保任何需要擴充介面的類別均實作完全相同的方法,方法簽章也都一致。如果您以任何方式修改介面,例如新增或移除屬性/方法或變更方法簽名),編譯器會要求您更新任何擴充介面的類別,讓程式碼保持一致且更易於維護。

介面允許擴充類別的行為發生變化。根據各類別來提供實作。

我們來看看如何重寫進度列以使用介面,然後使用測驗類別來擴充該介面。

  1. Quiz 類別上方,定義名為 ProgressPrintable 的介面。我們已選擇名稱 ProgressPrintable,因為可以使用擴充的任何類別,使其能夠列印進度列。
interface ProgressPrintable {
}
  1. ProgressPrintable 介面中,定義名為 progressText 的屬性。
interface ProgressPrintable {
    val progressText: String
}
  1. 修改 Quiz 類別的宣告,擴充 ProgressPrintable 介面。
class Quiz : ProgressPrintable {
    ...
}
  1. Quiz 類別中,新增名為 progressTextString 類型屬性,如 ProgressPrintable 介面所指定。由於該屬性來自 ProgressPrintable,請在 val 前面加上 override 關鍵字。
override val progressText: String
  1. 從舊版 progressText 擴充功能屬性複製屬性 getter。
override val progressText: String
        get() = "${answered} of ${total} answered"
  1. 移除舊有的 progressText 擴充功能屬性。

要刪除的程式碼︰

val Quiz.StudentProgress.progressText: String
    get() = "${answered} of ${total} answered"
  1. ProgressPrintable 介面中,新增名為 printProgressBar 的方法,該方法不採用任何參數,且沒有傳回值。
interface ProgressPrintable {
    val progressText: String
    fun printProgressBar()
}
  1. Quiz 類別中,使用 override 關鍵字來新增 printProgressBar() 方法。
override fun printProgressBar() {
}
  1. 將舊版 printProgressBar() 擴充功能函式的程式碼移至介面的新 printProgressBar()。移除對 Quiz 的參照,藉此修改最後一行,參照介面上新的 progressText 變數。
override fun printProgressBar() {
    repeat(Quiz.answered) { print("▓") }
    repeat(Quiz.total - Quiz.answered) { print("▒") }
    println()
    println(progressText)
}
  1. 移除擴充功能函式 printProgressBar()。這項功能現已屬於可擴充 ProgressPrintableQuiz 類別。

要刪除的程式碼︰

fun Quiz.StudentProgress.printProgressBar() {
    repeat(Quiz.answered) { print("▓") }
    repeat(Quiz.total - Quiz.answered) { print("▒") }
    println()
    println(Quiz.progressText)
}
  1. 更新 main() 中的程式碼。printProgressBar() 函式現在是 Quiz 類別的方法,因此您需要先將 Quiz 物件例項化,再呼叫 printProgressBar()
fun main() {
    Quiz().printProgressBar()
}
  1. 執行程式碼。輸出無變化,但程式碼現已模組化。隨著程式碼集不斷擴增,您可以輕鬆新增符合同一介面的類別,這樣就能重複使用程式碼,而不必從父類別繼承。
▓▓▓▒▒▒▒▒▒▒
3 of 10 answered.

介面的用途很多,可協助您建構程式碼,您將開始在常用單元中看到這些程式碼。以下是您在持續使用 Kotlin 時可能會遇到的介面範例。

  • 手動插入依附元件。建立一個介面,定義依附元件的所有屬性和方法。您需要這個介面做為依附元件的資料類型 (活動、測試案例等),才能使用實作此介面的類別例項。這可讓您替換基礎的實作。
  • 模擬自動化測試。模擬類別和實際類別與同一介面相符。
  • Compose Multiplatform 應用程式中存取相同的依附元件。例如,您可以建立一個介面,針對 Android 裝置和電腦提供一組通用的屬性和方法,即使每個平台的基礎實作不同。
  • Compose 中有幾種資料類型是介面 (例如 Modifier)。這可讓您新增修飾符,而無須存取或修改基礎原始碼。

8. 運用範圍函式存取類別屬性和方法

如您所看到的,Kotlin 提供多種讓程式碼更簡潔的功能。

隨著您繼續學習 Android 開發方法,之後會碰到名為「範圍函式」的功能。透過範圍函式,您可以簡要地存取類別中的屬性和方法,而不必重複存取變數名稱。這代表什麼意思?一起來看看這個範例。

運用範圍函式排除重複的物件參照

範圍函式是高排序的函式,可以不必參照物件名稱,就存取物件屬性和方法。稱為範圍函式的原因,在於傳入的函式內文會採用呼叫這類函式時所涉及的物件範圍。舉例來說,您可以使用某些範圍函式存取類別中的屬性和方法,如同這些函式是定義為該類別的方法一樣。您可以藉此省略一旦撰寫會稍嫌多餘的物件名稱,讓程式碼更容易判讀。

請看一些本課程之後會使用的範圍函數,以便您更容易瞭解它的功能。

運用 let() 取代冗長的物件名稱

let() 函式可以用修飾詞 it 參照 lambda 運算式裡面的物件,不需使用物件的實際名稱。當需要存取多種屬性時,這個函式就能幫助您避免多次使用冗長的詳細物件名稱。let() 函式是一種擴充功能函式,可以用點號標記法在任何 Kotlin 物件內呼叫。

嘗試藉由 let() 存取 question1question2question3 的屬性:

  1. Quiz 類別裡新增名為 printQuiz() 的函式。
fun printQuiz() {

}
  1. 新增以下程式碼,以便列印問題的 questionTextanswerdifficulty。存取 question1question2question3 的多種屬性時,每次都會使用完整的變數名稱。如果變數名稱有所變動,您就需要更新所有用到的地方。
fun printQuiz() {
    println(question1.questionText)
    println(question1.answer)
    println(question1.difficulty)
    println()
    println(question2.questionText)
    println(question2.answer)
    println(question2.difficulty)
    println()
    println(question3.questionText)
    println(question3.answer)
    println(question3.difficulty)
    println()
}
  1. 在存取 questionTextanswerdifficulty 屬性的程式碼前後加上在 question1question2question3 呼叫 let() 函式的程式碼。用 it 取代所有 lambda 運算式內的變數名稱。
fun printQuiz() {
    question1.let {
        println(it.questionText)
        println(it.answer)
        println(it.difficulty)
    }
    println()
    question2.let {
        println(it.questionText)
        println(it.answer)
        println(it.difficulty)
    }
    println()
    question3.let {
        println(it.questionText)
        println(it.answer)
        println(it.difficulty)
    }
    println()
}
  1. 更新 main() 中的程式碼,建立 Quiz 類別的例項並命名為 quiz
fun main() {
    val quiz = Quiz()
}
  1. 呼叫 printQuiz()
fun main() {
    val quiz = Quiz()
    quiz.printQuiz()
}
  1. 執行程式碼,確認運作正常。
Quoth the raven ___
nevermore
MEDIUM

The sky is green. True or false
false
EASY

How many days are there between full moons?
28
HARD

運用 apply() 呼叫不含變數的物件方法

範圍函式有一項厲害的功能,可在將物件指派給變數前,就對物件呼叫這類函式。舉例來說,apply() 函式是一種擴充功能函式,可以用點號標記法在物件內呼叫。apply() 函式也會回傳該物件的參照,以便儲存在變數裡。

更新 main() 的程式碼,以便呼叫 apply() 函式。

  1. 在建立 Quiz 類別的執行個體時,在右括號後方呼叫 apply()。呼叫 apply() 時可以省略括號,並在後方使用 lambda 語法。
val quiz = Quiz().apply {
}
  1. printQuiz() 的呼叫內容移到 lambda 運算式裡面。您已經不需要再參照 quiz 變數或使用點號標記法了。
val quiz = Quiz().apply {
    printQuiz()
}
  1. apply() 函式會回傳 Quiz 類別的執行個體,不過既然 quiz 變數已經沒有用處,您可以移除這個變數。使用 apply() 函式之後,不需使用變數,也能在 Quiz 執行個體呼叫方法。
Quiz().apply {
    printQuiz()
}
  1. 執行程式碼。請注意,您不必參照 Quiz 例項,就能呼叫這個方法。apply() 函式會回傳 quiz 裡儲存的物件。
Quoth the raven ___
nevermore
MEDIUM

The sky is green. True or false
false
EASY

How many days are there between full moons?
28
HARD

雖然不必使用範圍函式也能達到想要的輸出結果,但以上範例說明了如何運用這個方式讓程式碼更精簡,且能避免重複使用相同的變數名稱。

以上只有兩個程式碼的範例,建議您將範圍函式文件設為書籤,後續課程中如果再看到這個函式,可以打開來參考。

9. 摘要

這次您已看到 Kotlin 新功能的實際運作情況。泛型可以讓資料類型用參數的形式傳遞到類別內,列舉類別可以定義有限數量的可能值,而資料類別能夠協助系統自動為類別產生實用的方法。

您也學到如何建立將執行個體限制為一個的單例模式物件,以及如何把物件設為其他類別的伴生物件,還有如何用新的 get 限定屬性及新方法來擴充現有的類別。最後也提供了幾個範例,說明範圍函式如何簡化存取屬性和方法的語法。

隨著您繼續學習 Kotlin、Android 開發方法和 Compose,將會在後續單元中學到這些概念。您現在已經對這些功能的運作方式有進一步的理解,也更清楚知道這些功能如何增進程式碼的重複利用能力和容易判讀的程度。

10. 瞭解詳情