Tina Tang's Blog

在哪裡跌倒了,就在哪裡躺下來

0%

Android筆記(16)-在Kotlin中使用List

了解如何在在 Kotlin 中創建 List 並循環使用該 List。

學習目標

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

List 簡介

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

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

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

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

建立 List
  1. 開啟 Kotlin Playground,並刪除其中提供的現有程式碼。

  2. 新增空白的 main() 函式。下列所有程式碼步驟都會位於這個 main() 函式中。

1
2
3
fun main() {

}
  1. main() 中,建立類型為 List<Int>numbers 變數,因為其中包含整數的唯讀清單。使用 Kotlin 標準程式庫函式 listOf() 建立新的 List,然後將清單元素做為以半形逗號隔開的引數傳入。listOf(1, 2, 3, 4, 5, 6) 會傳回介於 1 到 6 之間的整數唯讀清單。
1
val numbers: List<Int> = listOf(1, 2, 3, 4, 5, 6)
  1. 如果根據指派運算子 (=) 右側的值可推斷 (或推論) 變數類型,則可忽略變數的資料類型。因此,您可以將這行程式碼縮短為以下內容:
1
val numbers = listOf(1, 2, 3, 4, 5, 6)
  1. 使用 println() 列印 numbers 列表。
1
println("List: $numbers")
  • 請記住,在字串中加入 $ 表示後面的內容是一個運算式,系統會評估該運算式並將其加入該字串 (請參閱字串範本)。這行程式碼也可寫成 println("List: " + numbers).
  1. 使用 numbers.size 屬性擷取 List 大小,並將其列印出來。
1
println("Size: ${numbers.size}")
  1. 執行程式。輸出結果會列出清單的所有元素和清單的大小。請注意,括號 [] 代表這是 List。括號內為 numbers 的元素,以半形逗號分隔。另請注意,這些元素的順序與其建立順序相同。
1
2
List: [1, 2, 3, 4, 5, 6]
Size: 6
存取 List 元素

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

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

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

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

  1. 列印 List 的最後一個元素,使用 numbers.size - 1 計算其索引,應為 5。存取第 5 個索引處的元素時,系統會傳回 6 做為輸出內容。
1
2
println("Last index: ${numbers.size - 1}")
println("Last element: ${numbers[numbers.size - 1]}")
  1. Kotlin 也支援在 List 上執行 first()last() 作業。請嘗試呼叫 numbers.first()numbers.last(),並查看輸出內容。
1
2
println("First: ${numbers.first()}")
println("Last: ${numbers.last()}")
  • numbers.first() 會傳回 List 的第一個元素,而 numbers.last() 會傳回 List 的最後一個元素。
  1. 另一個實用的 List 作業就是 contains() 方法,可確認 List 中是否有指定的元素。舉例來說,如果您有一份公司員工姓名清單,則可以使用 contains() 方法確認 List 中是否包含指定的姓名。

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

1
2
println("Contains 4? ${numbers.contains(4)}")
println("Contains 7? ${numbers.contains(7)}")
  1. 已完成的程式碼應如下所示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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. 執行程式碼。以下是輸出結果。
1
2
3
4
5
6
7
8
9
10
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 List 已初始化為一份包含 3 個顏色的清單,以 Strings 表示。
1
2
3
fun main() {
val colors = listOf("green", "orange", "blue")
}
  1. 請注意,您無法在唯讀 List 中新增或變更元素。看看如果嘗試將項目加入 List,或嘗試將 List 中的元素設定為新的值,藉此修改 List 元素,會發生什麼情況。
1
2
colors.add("purple")
colors[0] = "yellow"

執行程式碼,系統會顯示幾條錯誤訊息。基本上,這些錯誤表示 Listadd() 方法不存在,且您無法變更元素的值。

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

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

  1. 新增程式碼可反轉 colors List。列印輸出結果。這是一份新 List,其中包含了以相反順序排序的 colors 元素。

  2. 加入第二行程式碼可列印原始 List,如此您便能看到原始 List 並未變更。

1
2
println("Reversed list: ${colors.reversed()}")
println("List: $colors")

執行結果:

1
2
Reversed list: [blue, orange, green]
List: [green, orange, blue]
  1. 新增程式碼,使用 sorted() 函式傳回 List 的已排序版本。
1
println("Sorted list: ${colors.sorted()}")

執行結果:

1
Sorted list: [blue, green, orange]
  • 輸出內容是一份新的顏色 List,該 List 依字母順序排列。
  1. 也可以嘗試在未排序的數字清單上使用 sorted() 函式。
1
2
3
val oddNumbers = listOf(5, 3, 7, 1)
println("List: $oddNumbers")
println("Sorted list: ${oddNumbers.sorted()}")

執行結果:

1
2
List: [5, 3, 7, 1]
Sorted list: [1, 3, 5, 7]

可變動 List 簡介

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

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

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

1
Not enough information to infer type variable T

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

  1. 修正變數宣告,指定您要建立 String 類型的可變動 List。
1
val entrees = mutableListOf<String>()
  1. 另一個修正錯誤的方法,就是預先指定變數的資料類型。
1
val entrees: MutableList<String> = mutableListOf()
  • val 可用於可變動 List,因為 entrees 變數包含對 List 的參照;即使 List 內容有所變更,參照也不會改變。
  1. 列印 List。
1
println("Entrees: $entrees")

執行結果:

1
Entrees: []
  • 空白清單的輸出內容會顯示 []
在清單中新增元素

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

  1. 使用 entrees.add("noodles"). 將 “noodles” 新增至 List。如果成功將元素新增至 List,add() 函式會傳回 true,否則傳回 false

  2. 列印 List,確認確實已新增 “noodles”。

1
2
println("Add noodles: ${entrees.add("noodles")}")
println("Entrees: $entrees")

輸出內容如下:

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

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

1
2
Add spaghetti: true
Entrees: [noodles, spaghetti]

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

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

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

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

作業失敗,發生錯誤:

1
The integer literal does not conform to the expected type String

這是因為 entrees List 需要 String 類型的元素,而您嘗試新增的是 Int。請記住,僅在 List 中新增正確資料類型的元素。否則,您將收到編譯錯誤。

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

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

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

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

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

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

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

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

1
Empty? true

以下是以上可變動 List 撰寫的所有程式碼。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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()}")
}

迴圈 List

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

While 迴圈

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

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

注意: while 迴圈不必包含 List (這裡的範例),但適合 List 使用。

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

  1. 刪除 Kotlin Playground 中現有的程式碼,得到一個空白的 main() 函式。

  2. 假設您正在籌辦派對。建立一份 List,其中每個元素都代表每個家庭所回覆的賓客人數。第一個家庭表示自家會有 2 人參加。第二個家庭表示自家會有 4 人參加,依此類推。

1
val guestsPerFamily = listOf(2, 4, 1, 3)
  1. 確認賓客總數。撰寫迴圈找到答案。為賓客總數建立 var,並將其初始化為 0
1
var totalGuests = 0
  1. 初始化 index 變數的 var,如前文所述。
1
var index = 0
  1. 撰寫 while 迴圈,以迭代整個清單。條件是只要 index 值小於 List 的大小,系統就會一直執行程式碼區塊。
1
2
3
while (index < guestsPerFamily.size) {

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

迴圈的最後一行會使用 index++index 變數遞增 1,這樣下一個迴圈迭代會查看 List 中的下一個家庭。

1
2
3
4
while (index < guestsPerFamily.size) {
totalGuests += guestsPerFamily[index]
index++
}

我的程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
fun main() {
val guestsPerFamily = listOf(2, 4, 1, 3)
var totalGuests = 0
var index = 0

while (index < guestsPerFamily.size) {
println("index: $index")
totalGuests += guestsPerFamily[index]
println("totalGuests: $totalGuests")
index ++
}
}

執行結果:

1
2
3
4
5
6
7
8
index: 0
totalGuests: 2
index: 1
totalGuests: 6
index: 2
totalGuests: 7
index: 3
totalGuests: 10
  • 根據以上結果,可以看出 index 加 1 時,totalGuest 也隨之增加。
  1. 您可以在 while 迴圈之後列印輸出結果。
1
2
3
4
while ... {
...
}
println("Total Guest Count: $totalGuests")
  1. 執行程式,輸出內容如下。只需手動把 List 中的數字加起來,就能驗證正確答案。
1
Total Guest Count: 10

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

1
2
3
4
5
6
7
8
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")
For 迴圈

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

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

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

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

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

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

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

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

輸出內容:

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

注意: 以下是您可以使用 for 迴圈執行的其他一些變體,包括將它們用於具有特定步驟的範圍 (而不是每次遞增 1)。
for (item in list) print(item) // Iterate over items in a list
for (item in 'b'..'g') print(item) // Range of characters in an alphabet
for (item in 1..5) print(item) // Range of numbers
for (item in 5 downTo 1) print(item) // Going backward
for (item in 3..6 step 2) print(item) // Prints: 35

您可以在本程式碼研究室結尾的說明文件中找到更多資訊。


靈活運用

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

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

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

1
2
3
4
5
6
7
8
Order #1
Noodles: $10
Total: $10

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

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
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. 您應該會看到類似以下的輸出內容:
1
2
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 屬性。
1
2
3
4
5
class Noodles : Item("Noodles", 10) {
override fun toString(): String {
return name
}
}
  1. 針對 Vegetables 類別重複相同步驟。
1
2
3
4
5
class Vegetables() : Item("Vegetables", 5) {
override fun toString(): String {
return name
}
}
  1. 執行程式碼。現在,輸出內容看起來更有用:
1
2
Noodles
Vegetables
透過訂單自訂蔬菜

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

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

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

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

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

  1. 更新 Vegetables 類別標頭,以擷取 3 個字串參數,如以下程式碼所示:
1
2
3
class Vegetables(val topping1: String,
val topping2: String,
val topping3: String) : Item ("Vegetables", 5) {
  1. 現在,您的程式碼會重新編譯。不過,只有客戶想每次都訂購三種蔬菜時,這個解決方法才適用。如果客戶想訂購一種或五種蔬菜,就沒有辦法了。

  2. 您可以在 Vegetables 類別的建構函式中接受一份蔬菜清單 (長度不限) 以修正此問題,而不用使用每個蔬菜的屬性。List 只能包含 Strings,因此輸入參數的類型為 List<String>

1
class Vegetables(val toppings: List<String>) : Item("Vegetables", 5) {

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

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

還有更好的解決方法。

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

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

1
class Vegetables(vararg val toppings: String) : Item("Vegetables", 5) {

注意: 您只能將一個參數標示為 vararg,且通常是 List 中的最後一個參數。

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

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

1
2
3
4
5
class Vegetables(vararg val toppings: String) : Item("Vegetables", 5) {
override fun toString(): String {
return name + " " + toppings.joinToString()
}
}
  1. 執行程式,輸出內容應為:
1
2
Noodles
Vegetables Cabbage, Sprouts, Onion

注意: 如要指定半形逗號以外的分隔符,請將所需的分隔符字串做為引數傳遞至 joinToString() 方法。範例:joinToString(" ") 以空格分隔每個商品。

  1. 編寫程式時,您需要考量所有可能的輸入內容。如果 Vegetables 建構函式中沒有任何輸入引數,請使用較容易的方式處理 toString() 方法。

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

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

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

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

  1. 想想看 Order 類別可以使用哪些屬性和方法。如果有所幫助,請再次參考以下的最終程式碼輸出內容範例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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

1
class Order(val orderNumber: Int)
  1. 您可能無法預先知道訂單中的所有商品,因此無需將商品 List 作為引數傳遞。這可以宣告為頂層類別變數,並初始化為空白的 MutableList,用於儲存 Item 類型的元素。標示變數 private,讓系統只允許這個類別直接修改商品 List。這種做法可以防止此類別以外的程式碼意外修改清單。
1
2
3
class Order(val orderNumber: Int) {
private val itemList = mutableListOf<Item>()
}
  1. 現在,請將方法加入類別定義中。您可以隨意為每種方法挑選合理的名稱,目前每個方法內的實作邏輯則可以留空。此外,還要決定需要哪些函式引數和傳回值。
1
2
3
4
5
6
7
8
9
10
11
12
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
1
2
3
fun addItem(newItem: Item) {
itemList.add(newItem)
}
  1. 接下來請實作 addAll() 方法。該方法會擷取唯讀商品 List。將所有商品加入內部商品 List。
1
2
3
fun addAll(newItems: List<Item>) {
itemList.addAll(newItems)
}
  1. 接著,請實作 print() 方法,藉此將所有商品及其價格的摘要以及訂單的總價輸出至輸出內容。
    請先列印輸出訂單號碼。接下來,使用迴圈迭代訂單清單中的所有商品。請輸出每個商品及其對應價格。同時,保留到目前為止的總價,並在疊代整個清單時繼續增加總價。最後,輸出總價。嘗試自行實作這個邏輯。如需相關協助,請查看下列解決方法。

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

1
2
3
4
5
6
7
8
9
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 例項,依此類推。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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. 上述程式碼的輸出內容應如下所示。確認總價已正確相加。
1
2
3
4
5
6
7
8
9
10
11
12
13
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

改善程式碼

保留訂單清單

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

  1. 建立 List 以儲存所有訂單。它是唯讀 List 還是可變動 List 嗎?

  2. 將這段程式碼新增至 main() 函式。首先,請將 List 初始化為空白。接著,每次建立訂單時,將訂單加入 List 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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)
}

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

  1. 建立訂單 List 後,您可以使用迴圈來列印每個訂單。在訂單之間列印空白行,讓輸出內容更容易閱讀。
1
2
3
4
5
6
7
8
9
10
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
1
2
3
4
5
6
7
8
9
fun addItem(newItem: Item): Order {
itemList.add(newItem)
return this
}

fun addAll(newItems: List<Item>): Order {
itemList.addAll(newItems)
return this
}
  1. 現在,在 main() 函式中,您可以將呼叫鏈結在一起,如以下程式碼所示。這個程式碼會建立新的 Order,並充分運用建構工具模式。
1
2
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。如果每種方法呼叫都自成一行,程式碼也更容易讀取。
1
2
3
4
5
ordersList.add(
Order(5)
.addItem(Noodles())
.addItem(Noodles())
.addItem(Vegetables("Spinach")))
  1. 執行程式碼,以下是預期的輸出內容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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

所有程式碼

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
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()
}
}

總結

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

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

    }
  • 使用 for 迴圈疊代清單的所有商品:
    1
    2
    3
    4
    5
    for (item in myList) {

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

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