在 Kotlin 中使用清單

1. 事前準備

我們常會在日常生活中為各種情況製作清單,例如待辦事項清單、活動邀請對象清單、願望清單或雜貨清單。在程式設計中,清單也非常實用。舉例來說,應用程式中可能有新聞文章、歌曲、日曆活動或社群媒體貼文等清單。

學習如何建立及使用清單是重要的程式設計概念,可以運用在工具箱中,您可以藉此建立更為複雜的應用程式。

在本程式碼研究室中,您將使用 Kotlin Playground 熟悉 Kotlin 中的清單,並建立程式來訂購不同種類的湯麵。餓了嗎?

必要條件

  • 熟悉使用 Kotlin Playground 來建立及編輯 Kotlin 程式。
  • 熟悉 Kotlin 課程 Android 基礎知識中第 1 單元的基本 Kotlin 程式設計概念:main()函式、函式引數和傳回值、變數、資料類型和作業,以及控制流程陳述式。
  • 能夠定義 Kotlin 類別、從中建立物件例項,以及存取其屬性和方法。
  • 能夠建立子類別,並瞭解各個子類別彼此繼承的方式。

課程內容

  • 如何在 Kotlin 中建立及使用清單
  • ListMutableList 的差異,以及兩者的使用時機
  • 如何疊代清單中的所有項目,並對每個項目執行動作。

建構項目

  • 您要在 Kotlin Playground 中嘗試使用清單和清單作業。
  • 您要在 Kotlin Playground 中建立使用清單的訂餐程式。
  • 您的程式能夠建立訂單、向訂單中加入麵條和蔬菜,然後計算訂單的總費用。

需求條件

2. 清單簡介

在先前的程式碼研究室中,您已瞭解 Kotlin 的基本資料類型,例如 IntDoubleBooleanString。這些資料類型可讓您在變數中儲存特定類型的值。但如要儲存多個值,該怎麼辦?這時我們就需要 List 資料類型。

清單是具有特定順序的項目的集合。Kotlin 中有兩種類型的清單:

  • 唯讀清單:List 建立後即無法修改。
  • 可變動清單:MutableList 在建立後可以修改,也就是說,您可以新增、移除或更新其元素。

使用 ListMutableList 時,您必須指定它能夠包含的元素類型。例如,List<Int> 包含整數清單,List<String> 則包含字串清單。如果您在程式中定義 Car 類別,則可擁有 List<Car>,其中包含 Car 物件例項的清單。

瞭解清單的最佳方式就是試用清單。

建立清單

  1. 開啟 Kotlin Playground,並刪除其中提供的現有程式碼。
  2. 新增空白的 main() 函式。下列所有程式碼步驟都會位於這個 main() 函式中。
fun main() {

}
  1. main() 中,建立類型為 List<Int>numbers 變數,因為其中包含整數的唯讀清單。使用 Kotlin 標準程式庫函式 listOf() 建立新的 List,然後將清單元素做為以半形逗號隔開的引數傳入。listOf(1, 2, 3, 4, 5, 6) 會傳回介於 1 到 6 之間的整數唯讀清單。
val numbers: List<Int> = listOf(1, 2, 3, 4, 5, 6)
  1. 如果根據指派運算子 (=) 右側的值可推斷 (或推論) 變數類型,則可忽略變數的資料類型。因此,您可以將這行程式碼縮短為以下內容:
val numbers = listOf(1, 2, 3, 4, 5, 6)
  1. 使用 println() 列印 numbers 清單。
println("List: $numbers")

請記住,在字串中加入 $ 表示後面的內容是一個運算式,系統會評估該運算式並將其加入該字串 (請參閱字串範本)。這行程式碼也可寫成 println("List: " + numbers).

  1. 使用 numbers.size 屬性擷取清單大小,並將其列印出來。
println("Size: ${numbers.size}")
  1. 執行程式。輸出結果會列出清單的所有元素和清單的大小。請注意,括號 [] 代表這是 List。括號內為 numbers 的元素,以半形逗號分隔。另請注意,這些元素的順序與其建立順序相同。
List: [1, 2, 3, 4, 5, 6]
Size: 6

存取清單元素

清單的特定功能是,您能夠依元素的索引存取清單的每個元素,索引是代表位置的整數。下圖是我們建立的 numbers 清單圖表,其中顯示了每個元素及其對應的索引。

cb6924554804458d.png

索引實際上是與第一個元素的偏移值。例如,當您表示 list[2] 時,您並非要求清單的第二個元素,而是要求與第一個元素偏移 2 個位置的元素。因此,list[0] 是第一個元素 (零偏移),list[1] 是第二個元素 (偏移值 1),list[2] 是第三個元素 (偏移值 2),依此類推。

main() 函式中現有程式碼的後方加上以下程式碼。請在完成每一步後執行程式碼,以便驗證輸出結果是否正確無誤。

  1. 列印清單中索引為 0 的第一個元素。您可以呼叫 get() 函式,使得所需索引為 numbers.get(0),也可以使用簡式語法搭配索引前後的方括號做為 numbers[0]
println("First element: ${numbers[0]}")
  1. 接下來,列印清單中索引為 1 的第二個元素。
println("Second element: ${numbers[1]}")

清單的有效索引值 (「索引」) 介於 0 到最後一個索引之間,也就是清單大小減 1。也就是說,您的 numbers 清單中的索引介於 0 至 5 之間。

  1. 列印清單的最後一個元素,使用 numbers.size - 1 計算其索引,應為 5。存取第 5 個索引處的元素時,系統會傳回 6 做為輸出內容。
println("Last index: ${numbers.size - 1}")
println("Last element: ${numbers[numbers.size - 1]}")
  1. Kotlin 也支援在清單上執行 first()last() 作業。請嘗試呼叫 numbers.first()numbers.last(),並查看輸出內容。
println("First: ${numbers.first()}")
println("Last: ${numbers.last()}")

您會注意到,numbers.first() 會傳回清單的第一個元素,而 numbers.last() 會傳回清單的最後一個元素。

  1. 另一個實用的清單作業就是 contains() 方法,可確認清單中是否有指定的元素。舉例來說,如果您有一份公司員工姓名清單,則可以使用 contains() 方法確認清單中是否包含指定的姓名。

numbers 清單中,呼叫 contains() 方法,並提供清單中的一個整數。numbers.contains(4) 會傳回 true 值。接著,呼叫 contains() 方法,並提供一個不存在於清單中的整數。numbers.contains(7) 會傳回 false

println("Contains 4? ${numbers.contains(4)}")
println("Contains 7? ${numbers.contains(7)}")
  1. 已完成的程式碼應如下所示。您可選擇留言。
fun main() {
    val numbers = listOf(1, 2, 3, 4, 5, 6)
    println("List: $numbers")
    println("Size: ${numbers.size}")

    // Access elements of the list
    println("First element: ${numbers[0]}")
    println("Second element: ${numbers[1]}")
    println("Last index: ${numbers.size - 1}")
    println("Last element: ${numbers[numbers.size - 1]}")
    println("First: ${numbers.first()}")
    println("Last: ${numbers.last()}")

    // Use the contains() method
    println("Contains 4? ${numbers.contains(4)}")
    println("Contains 7? ${numbers.contains(7)}")
}
  1. 執行程式碼。以下是輸出結果。
List: [1, 2, 3, 4, 5, 6]
Size: 6
First element: 1
Second element: 2
Last index: 5
Last element: 6
First: 1
Last: 6
Contains 4? true
Contains 7? false

清單為唯讀狀態

  1. 刪除 Kotlin Playground 中的程式碼,並替換成以下程式碼。colors 清單已初始化為一份包含 3 個顏色的清單,以 Strings 表示。
fun main() {
    val colors = listOf("green", "orange", "blue")
}
  1. 請注意,您無法在唯讀 List 中新增或變更元素。看看如果嘗試將項目加入清單,或嘗試將清單中的元素設定為新的值,藉此修改清單元素,會發生什麼情況。
colors.add("purple")
colors[0] = "yellow"
  1. 執行程式碼,系統會顯示幾條錯誤訊息。基本上,這些錯誤表示 Listadd() 方法不存在,且您無法變更元素的值。

dd21aaccdf3528c6.png

  1. 移除不正確的程式碼。

您已經瞭解到,唯讀清單無法變更。不過,有些清單作業並不會變更清單,只會傳回新的清單。其中兩個是 reversed()sorted()reversed() 函式會傳回新的清單,其中元素會依相反順序排序;sorted() 會傳回新的清單,元素會以遞增順序排序。

  1. 新增程式碼可反轉 colors 清單。列印輸出結果。這是一份新清單,其中包含了以相反順序排序的 colors 元素。
  2. 加入第二行程式碼可列印原始 list,如此您便能看到原始清單並未變更。
println("Reversed list: ${colors.reversed()}")
println("List: $colors")
  1. 以下是這兩份列印清單的輸出內容。
Reversed list: [blue, orange, green]
List: [green, orange, blue]
  1. 新增程式碼,使用 sorted() 函式傳回 List 的已排序版本。
println("Sorted list: ${colors.sorted()}")

輸出內容是一份新的顏色清單,該清單依字母順序排列。太棒了!

Sorted list: [blue, green, orange]
  1. 您也可以嘗試在未排序的數字清單上使用 sorted() 函式。
val oddNumbers = listOf(5, 3, 7, 1)
println("List: $oddNumbers")
println("Sorted list: ${oddNumbers.sorted()}")
List: [5, 3, 7, 1]
Sorted list: [1, 3, 5, 7]

現在,您已瞭解能建立清單有多實用。不過,建立清單後,要是還能修改就更好了,因此,接下來要學的是可變動清單。

3. 可變動清單簡介

可變動清單在建立後可以修改。您可以新增、移除或變更項目。可變動清單提供唯讀清單的全部功能。可變動清單的類型為 MutableList,您可以透過呼叫 mutableListOf() 來建立。

建立 MutableList

  1. 刪除 main() 中的現有程式碼。
  2. main() 函式中,建立一個空白的可變動清單,並指派給名為 entreesval 變數。
val entrees = mutableListOf()

如果嘗試執行程式碼,就會發生下列錯誤。

Not enough information to infer type variable T

如先前所述,當您建立 MutableListList 時,Kotlin 會嘗試從傳遞的引數中推論清單中包含的元素類型。舉例來說,當您編寫 listOf("noodles") 時,Kotlin 會推論您要建立 String 清單。初始化不含元素的空白清單時,Kotlin 無法推論元素的類型,因此您必須明確指出類型。為此,只要在 mutableListOflistOf 後的角括號中加上類型即可。(在說明文件中,這可能會顯示為 <T>,其中 T 代表類型參數)。

  1. 修正變數宣告,指定您要建立 String 類型的可變動清單。
val entrees = mutableListOf<String>()

另一個修正錯誤的方法,就是預先指定變數的資料類型。

val entrees: MutableList<String> = mutableListOf()
  1. 列印清單。
println("Entrees: $entrees")
  1. 空白清單的輸出內容會顯示 []
Entrees: []

在清單中新增元素

新增、移除及更新元素時,可變動清單會變得非常有趣。

  1. 使用 entrees.add("noodles")."noodles" 新增至清單。如果成功將元素新增至清單,add() 函式會傳回 true,否則傳回 false
  2. 列印清單,確認確實已新增 "noodles"
println("Add noodles: ${entrees.add("noodles")}")
println("Entrees: $entrees")

輸出內容如下:

Add noodles: true
Entrees: [noodles]
  1. 將另一個項目 "spaghetti" 新增至清單。
println("Add spaghetti: ${entrees.add("spaghetti")}")
println("Entrees: $entrees")

產生的 entrees 清單現在包含兩個項目。

Add spaghetti: true
Entrees: [noodles, spaghetti]

與其使用 add() 逐一新增元素,您可以使用 addAll() 一次新增多個元素並傳入清單。

  1. 建立 moreItems 清單。您不需要變更這個清單,因此請將其設定為 val,不可變動。
val moreItems = listOf("ravioli", "lasagna", "fettuccine")
  1. 使用 addAll(),將新清單上的所有項目新增至 entrees。列印產生的清單。
println("Add list: ${entrees.addAll(moreItems)}")
println("Entrees: $entrees")

輸出內容顯示新增清單成功。entrees 清單現在共有 5 個項目。

Add list: true
Entrees: [noodles, spaghetti, ravioli, lasagna, fettuccine]
  1. 現在,請嘗試在這份清單中新增一個數字。
entrees.add(10)

作業失敗,發生錯誤:

The integer literal does not conform to the expected type String

這是因為 entrees 清單需要 String 類型的元素,而您嘗試新增的是 Int。請記住,僅在清單中新增正確資料類型的元素。否則,您將收到編譯錯誤。Kotlin 能透過這種方式確保您的程式碼在類型安全方面安全無虞。

  1. 請移除不正確的程式碼行,確保編譯程式碼。

移除清單中的元素

  1. 呼叫 remove() 即可將 "spaghetti" 從清單中移除。再次列印清單。
println("Remove spaghetti: ${entrees.remove("spaghetti")}")
println("Entrees: $entrees")
  1. 移除 "spaghetti" 會傳回 true,因為該元素存在於清單中,可以成功移除。清單現在還剩 4 個項目。
Remove spaghetti: true
Entrees: [noodles, ravioli, lasagna, fettuccine]
  1. 如果您嘗試移除清單中沒有的項目,會發生什麼事?請嘗試使用 entrees.remove("rice") 從清單中移除 "rice"
println("Remove item that doesn't exist: ${entrees.remove("rice")}")
println("Entrees: $entrees")

remove() 方法會傳回 false,因為元素不存在,所以無法移除。清單保持不變,還是只有 4 個項目。輸出內容:

Remove item that doesn't exist: false
Entrees: [noodles, ravioli, lasagna, fettuccine]
  1. 您還可以指定要移除的元素索引。使用 removeAt() 移除索引 0 處的項目。
println("Remove first element: ${entrees.removeAt(0)}")
println("Entrees: $entrees")

removeAt(0) 的傳回值是從清單中移除的第一個元素 ("noodles")。entrees 清單現在還剩 3 個項目。

Remove first element: noodles
Entrees: [ravioli, lasagna, fettuccine]
  1. 如要清除整個清單,請呼叫 clear()
entrees.clear()
println("Entrees: $entrees")

輸出內容現在會顯示空白清單。

Entrees: []
  1. 透過 Kotlin,您可使用 isEmpty() 函式,檢查清單是否為空白。請嘗試列印輸出 entrees.isEmpty().
println("Empty? ${entrees.isEmpty()}")

輸出結果應為 true,因為清單目前為空白,沒有元素。

Empty? true

如果您想對清單執行作業或想存取特定元素,可以使用 isEmpty() 方法,但建議您先確保清單並非空白。

以下是您為可變動清單撰寫的所有程式碼。您可選擇留言。

fun main() {
    val entrees = mutableListOf<String>()
    println("Entrees: $entrees")

    // Add individual items using add()
    println("Add noodles: ${entrees.add("noodles")}")
    println("Entrees: $entrees")
    println("Add spaghetti: ${entrees.add("spaghetti")}")
    println("Entrees: $entrees")

    // Add a list of items using addAll()
    val moreItems = listOf("ravioli", "lasagna", "fettuccine")
    println("Add list: ${entrees.addAll(moreItems)}")
    println("Entrees: $entrees")

    // Remove an item using remove()
    println("Remove spaghetti: ${entrees.remove("spaghetti")}")
    println("Entrees: $entrees")
    println("Remove item that doesn't exist: ${entrees.remove("rice")}")
    println("Entrees: $entrees")

    // Remove an item using removeAt() with an index
    println("Remove first element: ${entrees.removeAt(0)}")
    println("Entrees: $entrees")

    // Clear out the list
    entrees.clear()
    println("Entrees: $entrees")

    // Check if the list is empty
    println("Empty? ${entrees.isEmpty()}")
}

4. 迴圈清單

如要對清單中的每個項目執行作業,您可以透過清單執行迴圈 (也就是疊代整個清單)。迴圈功能可與 ListsMutableLists 搭配使用。

迴圈時

其中一種迴圈類型為 while 迴圈。在 Kotlin 中,while 迴圈以 while 關鍵字開頭。迴圈中包含一個程式碼區塊 (位於大括號中),只要括號中的運算式為 true 即可不斷執行。為避免程式碼永久執行 (又稱「無限迴圈」),程式碼區塊必須包含用來變更運算式值的邏輯,這樣一來,運算式最終將產生 false,系統將停止執行迴圈。屆時,系統可結束 while 迴圈,並繼續執行該迴圈之後的程式碼。

while (expression) {
    // While the expression is true, execute this code block
}

使用 while 迴圈疊代整個清單。建立變數,以在清單中追蹤您目前查看的 index。這個 index 變數會保持每次增加 1,直到達到清單的最後一個索引,然後您便可結束迴圈。

  1. 刪除 Kotlin Playground 中現有的程式碼,得到一個空白的 main() 函式。
  2. 假設您正在籌辦派對。建立一份清單,其中每個元素都代表每個家庭所回覆的賓客人數。第一個家庭表示自家會有 2 人參加。第二個家庭表示自家會有 4 人參加,依此類推。
val guestsPerFamily = listOf(2, 4, 1, 3)
  1. 確認賓客總數。撰寫迴圈找到答案。為賓客總數建立 var,並將其初始化為 0
var totalGuests = 0
  1. 初始化 index 變數的 var,如前文所述。
var index = 0
  1. 撰寫 while 迴圈,以疊代整個清單。條件是只要 index 值小於清單的大小,系統就會一直執行程式碼區塊。
while (index < guestsPerFamily.size) {

}
  1. 在迴圈中,請在目前的 index 處取得清單元素,並將該元素加入賓客變數總數。請注意,totalGuests += guestsPerFamily[index]totalGuests = totalGuests + guestsPerFamily[index]. 相同

請注意,迴圈的最後一行會使用 index++index 變數遞增 1,這樣下一個迴圈疊代會查看清單中的下一個家庭。

while (index < guestsPerFamily.size) {
    totalGuests += guestsPerFamily[index]
    index++
}
  1. while 迴圈之後,您可以列印輸出結果。
while ... {
    ...
}
println("Total Guest Count: $totalGuests")
  1. 執行程式,輸出內容如下。只需手動把清單中的數字加起來,就能驗證正確答案。
Total Guest Count: 10

以下是完整的程式碼片段:

val guestsPerFamily = listOf(2, 4, 1, 3)
var totalGuests = 0
var index = 0
while (index < guestsPerFamily.size) {
    totalGuests += guestsPerFamily[index]
    index++
}
println("Total Guest Count: $totalGuests")

使用 while 迴圈時,您必須編寫程式碼來建立變數,以便追蹤索引、取得清單中索引處的元素,以及更新該索引變數。要疊代整個清單,您可以使用一種更快速、精簡的方法。使用 for 迴圈!

For 迴圈

for 迴圈是另一種類型的迴圈。這可讓迴圈清單作業更加輕鬆。這會以 Kotlin 中的 for 關鍵字開頭,在大括號中放置程式碼區塊。用來執行程式碼區塊的條件在括號中表示。

for (number in numberList) {
   // For each element in the list, execute this code block
}

在這個範例中,變數 number 設為等於 numberList 的第一個元素,並執行程式碼區塊。接著,number 變數會自動更新為 numberList 的下一個元素,然後再次執行程式碼區塊。對清單的每個元素重複此操作,直到觸及 numberList 的結尾為止。

  1. 刪除 Kotlin Playground 中的現有程式碼,並替換成以下程式碼:
fun main() {
    val names = listOf("Jessica", "Henry", "Alicia", "Jose")
}
  1. 新增 for 迴圈,以輸出 names 清單中的所有項目。
for (name in names) {
    println(name)
}

這比直接撰寫為 while 迴圈要簡單許多!

  1. 輸出內容如下:
Jessica
Henry
Alicia
Jose

清單的一個常見作業就是對每個清單元素嘗試執行某個操作。

  1. 修改迴圈,使其列印輸出人員姓名的字元數。提示:您可以使用 Stringlength 屬性找出 String 中的字元數。
val names = listOf("Jessica", "Henry", "Alicia", "Jose")
for (name in names) {
    println("$name - Number of characters: ${name.length}")
}

輸出內容:

Jessica - Number of characters: 7
Henry - Number of characters: 5
Alicia - Number of characters: 6
Jose - Number of characters: 4

迴圈中的程式碼並未變更原始 List。這只會影響列印的內容。

為 1 個清單項目作業編寫指示的方式相當實用,而且每個清單項目都將執行程式碼!使用迴圈功能可避免重複輸入許多相同的程式碼。

現在您已經體驗了建立及使用清單和可變動清單,也對迴圈有所瞭解,接下來在範例使用案例中運用這些知識吧!

5. 靈活運用

在當地餐廳訂餐時,客戶通常會一次訂購多種商品。清單最適合用來儲存訂單相關資訊。此外,您也可運用類別和繼承的知識,來建立更完善、可擴充的 Kotlin 程式,而不是將所有程式碼放在 main() 函式中。

在接下來的一系列工作中,請建立一個 Kotlin 程式,用於訂購不同的食物組合。

首先請查看最後一個程式碼的範例輸出內容。腦力激盪一下,想想您需要建立哪些類別,以協助妥善規劃所有資料?

Order #1
Noodles: $10
Total: $10

Order #2
Noodles: $10
Vegetables Chef's Choice: $5
Total: $15

從輸出內容中,您可發現:

  • 有一份訂單清單
  • 每筆訂單都有一個編號
  • 每筆訂單都可包含麵條和蔬菜等商品的清單
  • 每個商品都有價格
  • 每筆訂單都有總價,也就是個別商品價格的總和

您可以建立類別來代表 Order,也可以建立類別來代表每項食品,例如 NoodlesVegetables。您可能還會發現 NoodlesVegetables 有相似之處,因為這兩者均是食品,而且都有價格。您可以建立 Item 類別,其中包含 Noodle 類別和 Vegetable 類別都可以繼承的共用屬性。這樣一來,您就不用複製 Noodle 類別和 Vegetable 類別中的邏輯。

  1. 畫面上會顯示下列範例程式碼。專業開發人員經常需要閱讀其他人的程式碼,例如,當他們加入新專案,或投入其他人建立的功能時。閱讀及理解程式碼是一項重要技能。

請花點時間檢查該程式碼,瞭解具體情況。複製這段程式碼並貼到 Kotlin Playground 中執行。在貼上這段新程式碼之前,請務必刪除 Kotlin Playground 中的所有現有程式碼。觀察輸出內容,看看它是否有助於您進一步理解程式碼。

open class Item(val name: String, val price: Int)

class Noodles : Item("Noodles", 10)

class Vegetables : Item("Vegetables", 5)

fun main() {
    val noodles = Noodles()
    val vegetables = Vegetables()
    println(noodles)
    println(vegetables)
}
  1. 您應該會看到類似以下的輸出內容:
Noodles@5451c3a8
Vegetables@76ed5528

以下是程式碼更為詳細的說明。首先是一個名為 Item 的類別,其中建構函式會使用 2 個參數:用於商品的 name (做為字串) 和 price (做為整數)。這兩項屬性在傳遞後皆維持不變,因此標示為 val。由於 Item 是父項類別,子類別由它擴充而來,因此該類別會標上 open 關鍵字。

Noodles 類別建構函式不含任何參數,但會從 Item 擴充,並透過傳遞 "Noodles" (做為名稱) 與價格 10 呼叫父類別建構函式。Vegetables 類別相似,但會以 "Vegetables" 與價格 5 呼叫父類別建構函式。

main() 函式會初始化 NoodlesVegetables 類別的新物件例項,並將其列印至輸出內容。

覆寫 toString() 方法

當您將物件例項列印至輸出內容時,系統會呼叫物件的 toString() 方法。在 Kotlin 中,每個類別都會自動繼承 toString() 方法。這個方法的預設實作只會傳回物件類型,其中包含例項的記憶體位址。建議您覆寫 toString(),以便傳回比 Noodles@5451c3a8Vegetables@76ed5528 更有意義且容易使用的結果。

  1. Noodles 類別中,覆寫 toString() 方法,然後使其傳回 name。請注意,Noodles 會繼承父項類別 Itemname 屬性。
class Noodles : Item("Noodles", 10) {
   override fun toString(): String {
       return name
   }
}
  1. 針對 Vegetables 類別重複相同步驟。
class Vegetables() : Item("Vegetables", 5) {
   override fun toString(): String {
       return name
   }
}
  1. 執行程式碼。現在,輸出內容看起來更有用:
Noodles
Vegetables

在下一個步驟中,您將變更 Vegetables 類別建構函式來擷取部分參數,並更新 toString() 方法以反映這些額外資訊。

透過訂單自訂蔬菜

為使麵條更有吸引力,您可以在訂單中包含不同的蔬菜。

  1. main() 函式中,不要初始化不含輸入引數的 Vegetables 例項,而是傳遞客戶想要的特定蔬菜。
fun main() {
    ...
    val vegetables = Vegetables("Cabbage", "Sprouts", "Onion")
    ...
}

如果現在嘗試編譯程式碼,系統會顯示以下錯誤訊息:

Too many arguments for public constructor Vegetables() defined in Vegetables

您正在將 3 個字串引數傳遞至 Vegetables 類別建構函式,因此需要修改 Vegetables 類別。

  1. 更新 Vegetables 類別標頭,以擷取 3 個字串參數,如以下程式碼所示:
class Vegetables(val topping1: String,
                 val topping2: String,
                 val topping3: String) : Item ("Vegetables", 5) {
  1. 現在,您的程式碼會重新編譯。不過,只有客戶想每次都訂購三種蔬菜時,這個解決方法才適用。如果客戶想訂購一種或五種蔬菜,就沒有辦法了。
  2. 您可以在 Vegetables 類別的建構函式中接受一份蔬菜清單 (長度不限) 以修正此問題,而不用使用每個蔬菜的屬性。List 只能包含 Strings,因此輸入參數的類型為 List<String>
class Vegetables(val toppings: List<String>) : Item("Vegetables", 5) {

這不是最完美的解決方法,因為在 main() 中,您需要先變更程式碼來建立配料清單,然後才將其傳遞至 Vegetables 建構函式。

Vegetables(listOf("Cabbage", "Sprouts", "Onion"))

還有更好的解決方法。

  1. 在 Kotlin 中,vararg 修改程式可讓您將類型相同、數量可變的引數傳遞給函式或建構函式。這樣一來,您就可以提供不同的蔬菜做為單個字串,而非清單。

變更 Vegetables 的類別定義,以採用類型為 Stringvararg toppings

class Vegetables(vararg val toppings: String) : Item("Vegetables", 5) {
  1. main() 函式中的程式碼現在正常運作。透過傳遞任何數量的配料字串,即可建立 Vegetables 例項。
fun main() {
    ...
    val vegetables = Vegetables("Cabbage", "Sprouts", "Onion")
    ...
}
  1. 現在,請修改 Vegetables 類別的 toString() 方法,使其傳回同樣提及以下配料格式的 StringVegetables Cabbage, Sprouts, Onion

以商品名稱 (Vegetables) 開頭。接著使用 joinToString() 方法將所有配料加入單一字串。使用 + 運算子將兩個部分加起來,之間留有空格。

class Vegetables(vararg val toppings: String) : Item("Vegetables", 5) {
    override fun toString(): String {
        return name + " " + toppings.joinToString()
    }
}
  1. 執行程式,輸出內容應為:
Noodles
Vegetables Cabbage, Sprouts, Onion
  1. 編寫程式時,您需要考量所有可能的輸入內容。如果 Vegetables 建構函式中沒有任何輸入引數,請使用較容易的方式處理 toString() 方法。

由於客戶想訂購蔬菜,但並未具體說明要哪些蔬菜,其中一種解決方法就是為他們提供預設由廚師選擇的蔬菜。

如未傳遞任何配料,請更新 toString() 方法以傳回 Vegetables Chef's Choice。使用您先前學過的 isEmpty() 方法。

override fun toString(): String {
    if (toppings.isEmpty()) {
        return "$name Chef's Choice"
    } else {
        return name + " " + toppings.joinToString()
    }
}
  1. 更新 main() 函數,以測試在兩種情況下建立 Vegetables 例項的可能性:不含任何建構函式引數以及含有多個引數。
fun main() {
    val noodles = Noodles()
    val vegetables = Vegetables("Cabbage", "Sprouts", "Onion")
    val vegetables2 = Vegetables()
    println(noodles)
    println(vegetables)
    println(vegetables2)
}
  1. 確認輸出內容符合預期。
Noodles
Vegetables Cabbage, Sprouts, Onion
Vegetables Chef's Choice

建立一個訂單

現在您擁有了一些食物,可以建立訂單了。在程式的 Order 類別中封裝訂單的邏輯。

  1. 想想看 Order 類別可以使用哪些屬性和方法。如果有所幫助,請再次參考以下的最終程式碼輸出內容範例。
Order #1
Noodles: $10
Total: $10

Order #2
Noodles: $10
Vegetables Chef's Choice: $5
Total: $15

Order #3
Noodles: $10
Vegetables Carrots, Beans, Celery: $5
Total: $15

Order #4
Noodles: $10
Vegetables Cabbage, Onion: $5
Total: $15

Order #5
Noodles: $10
Noodles: $10
Vegetables Spinach: $5
Total: $25
  1. 您可能已經想到以下幾點:

訂單類別

屬性:訂單號碼、商品清單

方法:新增商品、新增多個商品、列印訂單摘要 (含價格)

  1. 首先關注屬性,每個屬性的資料類型應該是什麼?它們對於類別應為公開還是私人?它們應該做為引數傳遞還是在類別中定義?
  2. 您可以透過多種方式來實作,以下是一個解決方法。建立含有整數 orderNumber 建構函式參數的 class Order
class Order(val orderNumber: Int)
  1. 您可能無法預先知道訂單中的所有商品,因此無需將商品清單作為引數傳遞。這可以宣告為頂層類別變數,並初始化為空白的 MutableList,用於儲存 Item 類型的元素。標示變數 private,讓系統只允許這個類別直接修改商品清單。這種做法可以防止此類別以外的程式碼意外修改清單。
class Order(val orderNumber: Int) {
    private val itemList = mutableListOf<Item>()
}
  1. 現在,請將方法加入類別定義中。您可以隨意為每種方法挑選合理的名稱,目前每個方法內的實作邏輯則可以留空。此外,還要決定需要哪些函式引數和傳回值。
class Order(val orderNumber: Int) {
   private val itemList = mutableListOf<Item>()

   fun addItem(newItem: Item) {
   }

   fun addAll(newItems: List<Item>) {
   }

   fun print() {
   }
}
  1. addItem() 方法似乎最直接,因此先實作該函式。該函式會擷取新的 Item,而該方法應將其新增至 itemList
fun addItem(newItem: Item) {
    itemList.add(newItem)
}
  1. 接下來請實作 addAll() 方法。該方法會擷取唯讀商品清單。將所有商品加入內部商品清單。
fun addAll(newItems: List<Item>) {
    itemList.addAll(newItems)
}
  1. 接著,請實作 print() 方法,藉此將所有商品及其價格的摘要以及訂單的總價輸出至輸出內容。

請先列印輸出訂單號碼。接下來,使用迴圈疊代訂單清單中的所有商品。請輸出每個商品及其對應價格。同時,保留到目前為止的總價,並在疊代整個清單時繼續增加總價。最後,輸出總價。嘗試自行實作這個邏輯。如需相關協助,請查看下列解決方法。

建議您加入貨幣符號,讓輸出內容更容易閱讀。以下是實作該解決方法的一個方式。此程式碼使用 $ 貨幣符號,但也可以視需要換算成當地幣別符號。

fun print() {
    println("Order #${orderNumber}")
    var total = 0
    for (item in itemList) {
        println("${item}: $${item.price}")
        total += item.price
    }
    println("Total: $${total}")
}

針對 itemList 中的每個 item,請輸出 item (這將觸發要在 item 上呼叫的 toString()),然後是商品的 price。同樣,在執行迴圈之前,請將 total 整數變數初始化為 0。接著,在 total 中加入目前商品的價格,繼續增加總價。

建立多個訂單

  1. main() 函式中建立 Order 例項,藉此測試程式碼。請先刪除 main() 函式中現有的內容。
  2. 您可以使用這些訂單範例,也可以自行建立訂單。嘗試使用訂單中各種不同的商品組合,確保測試了程式碼中的所有程式碼路徑。例如,在 Order 類別中測試 addItem()addAll() 方法,建立不含引數與含有引數的 Vegetables 例項,依此類推。
fun main() {
    val order1 = Order(1)
    order1.addItem(Noodles())
    order1.print()

    println()

    val order2 = Order(2)
    order2.addItem(Noodles())
    order2.addItem(Vegetables())
    order2.print()

    println()

    val order3 = Order(3)
    val items = listOf(Noodles(), Vegetables("Carrots", "Beans", "Celery"))
    order3.addAll(items)
    order3.print()
}
  1. 上述程式碼的輸出內容應如下所示。確認總價已正確相加。
Order #1
Noodles: $10
Total: $10

Order #2
Noodles: $10
Vegetables Chef's Choice: $5
Total: $15

Order #3
Noodles: $10
Vegetables Carrots, Beans, Celery: $5
Total: $15

做得好!現在看起來像是點餐了!

6. 改善程式碼

保留訂單清單

如果您建構能實際在麵店中使用的程式,請務必追蹤所有客戶訂單的清單。

  1. 建立清單以儲存所有訂單。它是唯讀清單還是可變動清單嗎?
  2. 將這段程式碼新增至 main() 函式。首先,請將清單初始化為空白。接著,每次建立訂單時,將訂單加入清單中。
fun main() {
    val ordersList = mutableListOf<Order>()

    val order1 = Order(1)
    order1.addItem(Noodles())
    ordersList.add(order1)

    val order2 = Order(2)
    order2.addItem(Noodles())
    order2.addItem(Vegetables())
    ordersList.add(order2)

    val order3 = Order(3)
    val items = listOf(Noodles(), Vegetables("Carrots", "Beans", "Celery"))
    order3.addAll(items)
    ordersList.add(order3)
}

由於訂單會隨著時間增加,因此清單應為 Order 類型的 MutableList。然後使用 MutableList 上的 add() 方法新增每個訂單。

  1. 建立訂單清單後,您可以使用迴圈來列印每個訂單。在訂單之間列印空白行,讓輸出內容更容易閱讀。
fun main() {
    val ordersList = mutableListOf<Order>()

    ...

    for (order in ordersList) {
        order.print()
        println()
    }
}

這麼做會移除 main() 函式中的重複程式碼,讓程式碼更容易閱讀!輸出內容應與先前相同。

實作訂單的建構工具模式

如果要讓 Kotlin 程式碼更簡潔,您可以使用建構工具模式來建立訂單。建構工具模式是程式設計中的設計模式,可逐步指導您建構複雜的物件。

  1. 請傳回已變更的 Order,不要傳回 Order 類別中 addItem()addAll() 方法的 Unit (或不傳回任何內容)。Kotlin 提供關鍵字 this 以參照目前的物件例項。在 addItem()addAll() 方法中,您會透過傳回 this 來傳回目前的 Order
fun addItem(newItem: Item): Order {
    itemList.add(newItem)
    return this
}

fun addAll(newItems: List<Item>): Order {
    itemList.addAll(newItems)
    return this
}
  1. 現在,在 main() 函式中,您可以將呼叫鏈結在一起,如以下程式碼所示。這個程式碼會建立新的 Order,並充分運用建構工具模式。
val order4 = Order(4).addItem(Noodles()).addItem(Vegetables("Cabbage", "Onion"))
ordersList.add(order4)

Order(4) 會傳回 Order 例項,您之後可以在上面呼叫 addItem(Noodles())addItem() 方法會傳回相同的 Order 例項 (採用新的狀態),您可以用再次在上面呼叫 addItem() 的方式處理蔬菜。傳回的 Order 結果可以儲存在 order4 變數中。

用來建立 Orders 的現有程式碼仍可使用,因此可保持不變。雖然並非一定要鏈結這些呼叫,但這是一種常見且推薦的做法,可讓您善用函式的傳回值。

  1. 此時您甚至不需要將訂單儲存在變數中。在 main() 函式中 (在列印輸出訂單的最終迴圈之前),直接建立 Order,並將其新增至 orderList。如果每種方法呼叫都自成一行,程式碼也更容易讀取。
ordersList.add(
    Order(5)
        .addItem(Noodles())
        .addItem(Noodles())
        .addItem(Vegetables("Spinach")))
  1. 執行程式碼,以下是預期的輸出內容:
Order #1
Noodles: $10
Total: $10

Order #2
Noodles: $10
Vegetables Chef's Choice: $5
Total: $15

Order #3
Noodles: $10
Vegetables Carrots, Beans, Celery: $5
Total: $15

Order #4
Noodles: $10
Vegetables Cabbage, Onion: $5
Total: $15

Order #5
Noodles: $10
Noodles: $10
Vegetables Spinach: $5
Total: $25

恭喜您完成本程式碼研究室!

現在,您已經瞭解了在清單中儲存資料、變動清單以及透過清單執行迴圈有多實用。在下一個程式碼研究室中,在 Android 應用程式中運用這些知識,以在畫面上顯示資料清單!

7. 解決方案程式碼

以下是 ItemNoodlesVegetablesOrder 類別的解決方案程式碼。此外,main() 函式也會顯示這些類別的使用方式。實作這項程式的方法有很多,因此您的程式碼可能會略有不同。

open class Item(val name: String, val price: Int)

class Noodles : Item("Noodles", 10) {
    override fun toString(): String {
        return name
    }
}

class Vegetables(vararg val toppings: String) : Item("Vegetables", 5) {
    override fun toString(): String {
        if (toppings.isEmpty()) {
            return "$name Chef's Choice"
        } else {
            return name + " " + toppings.joinToString()
        }
    }
}

class Order(val orderNumber: Int) {
    private val itemList = mutableListOf<Item>()

    fun addItem(newItem: Item): Order {
        itemList.add(newItem)
        return this
    }

    fun addAll(newItems: List<Item>): Order {
        itemList.addAll(newItems)
        return this
    }

    fun print() {
        println("Order #${orderNumber}")
        var total = 0
        for (item in itemList) {
            println("${item}: $${item.price}")
            total += item.price
        }
        println("Total: $${total}")
    }
}

fun main() {
    val ordersList = mutableListOf<Order>()

    // Add an item to an order
    val order1 = Order(1)
    order1.addItem(Noodles())
    ordersList.add(order1)

    // Add multiple items individually
    val order2 = Order(2)
    order2.addItem(Noodles())
    order2.addItem(Vegetables())
    ordersList.add(order2)

    // Add a list of items at one time
    val order3 = Order(3)
    val items = listOf(Noodles(), Vegetables("Carrots", "Beans", "Celery"))
    order3.addAll(items)
    ordersList.add(order3)

    // Use builder pattern
    val order4 = Order(4)
        .addItem(Noodles())
        .addItem(Vegetables("Cabbage", "Onion"))
    ordersList.add(order4)

    // Create and add order directly
    ordersList.add(
        Order(5)
            .addItem(Noodles())
            .addItem(Noodles())
            .addItem(Vegetables("Spinach"))
    )

    // Print out each order
    for (order in ordersList) {
        order.print()
        println()
    }
}

8. 摘要

Kotlin 提供的功能可協助您透過 Kotlin 標準資料庫輕鬆管理和操控資料集合。集合可定義為具有相同資料類型的多個物件。Kotlin 提供了不同的基本集合類型:清單、組合和對應。本程式碼研究室主要著重於清單,我們會在今後的程式碼研究室中詳細介紹組合和對應。

  • 清單是特定類型元素的有序集合,例如 Strings. 清單
  • 索引是反映元素位置的整數位置 (例如 myList[2])。
  • 在清單中,第一個元素位於索引 0 處 (例如 myList[0]),最後一個元素則位於 myList.size-1 處 (例如 myList[myList.size-1]myList.last())。
  • 清單分為兩種類型:ListMutableList.
  • List 處於唯讀狀態,在初始化後無法修改。不過,您可以執行 sorted()reversed() 等作業,這些作業可在不變更原始清單的情況下傳回新清單。
  • MutableList 建立後可以修改,例如新增、移除或修改元素。
  • 您可以使用 addAll() 將商品清單新增至可變動清單。
  • 使用 while 迴圈執行程式碼區塊,直到運算式評估為 false 並結束迴圈為止。

while (expression) {

// While the expression is true, execute this code block

}

  • 使用 for 迴圈疊代清單的所有商品:

for (item in myList) {

// Execute this code block for each element of the list

}

  • vararg 修飾符可讓您將數量可變的引數傳遞給函式或建構函式。

9. 瞭解詳情