在程式設計中,疊代指的是重複執行程式區塊多次的過程。你可以使用 while 迴圈和 until 迴圈等概念來達成,也可以使用遞迴,也就是一個會呼叫自己的函式。
不過,大多數時候你想疊代的是一組項目,例如Array、Range或String。Crystal 中的Array和Range類別都包含Enumerable模組,它提供了許多疊代元素的方法。而String類別則有自己的一套方法,用來疊代字元。
Crystal 也沒有其他語言那樣的 for 敘述。它改用幾個方法來進行疊代。
對集合進行疊代最常見的方式,是使用each方法,它會把集合中的每個元素傳給區塊。用Range就能輕鬆做到這點。假設你想在 1 到 3 之間迴圈,就可以用each方法疊代這個範圍。
(1..3).each do |n|
puts n
end
# Output:
# 1
# 2
# 3
更簡單的是,如果你只想疊代固定的次數,可以使用times方法,它定義在Int類別上。
3.times do |n|
puts n
end
# Output:
# 0
# 1
# 2
String
String是一串字元,不屬於Enumerable模組,也就是說它有自己的方法來疊代字元。疊代String最常見的方式是使用each_char方法,它會把String中的每個字元傳給區塊。
each_char方法傳給區塊的是Char物件,而不是String物件。
str = "hello"
str.each_char { |char| puts char }
# Output:
# h
# e
# l
# l
# o
疊代String的另一種方式是使用each_line方法。這個方法大多用在逐行讀取檔案的時候。
str = "hello\nworld"
str.each_line do |line|
puts line
end
# Output:
# hello
# world
Enumerable模組的物件Enumerable模組提供了許多方法,用來疊代集合中的元素。包含Enumerable模組的集合有Array、Range、Hash、Set等等,後面這些會在之後的概念中介紹。
疊代Array最常見的方式是使用each方法,它會把Array中的每個元素傳給區塊。
arr = [5, 2, 3]
arr.each do |element|
puts element
end
# Output:
# 5
# 2
# 3
map方法是疊代Array的另一種方式,它會回傳一個新的Array,裡面是把區塊套用到每個元素後的結果。它和each方法很類似,但會回傳一個包含轉換後元素的新Array。當你想把某個函式套用到集合中的每個元素時,轉換就非常有用,而這是很常見的操作。
arr = [1, 2, 3]
new_arr = arr.map do |element|
element * 2
end
new_arr
# => [2, 4, 6]
搭配if之類的控制流程,你還可以篩選要轉換哪些元素。
arr = [1, 2, 3]
new_arr = arr.map do |element|
if element.odd?
element * 2
else
element
end
end
new_arr
# => [2, 2, 6]
有時候你需要知道目前疊代到的元素索引,這時可以使用each_with_index或map_with_index方法。它會把每個元素和它的索引傳給區塊,這個方法也接受一個選用引數來指定起始索引。
arr = [1, 2, 3]
arr.each_with_index(4) do |element, index|
puts "Element: #{element}, Index: #{index}"
end
# Output:
# Element: 1, Index: 4
# Element: 2, Index: 5
# Element: 3, Index: 6
請注意,String物件也可以使用each_char_with_index方法。
sum方法會回傳集合中所有元素的總和,它也接受選用的區塊,在加總前先轉換元素。它還接受一個選用引數,用來指定總和的初始值。
arr = [1, 2, 3]
arr.sum
# => 6
arr.sum(2) { |n| n * 2 }
# => 14
reduce,在其他語言中稱為 fold,是一個搭配合併方法和初始值,把集合中所有元素合併起來的方法。reduce方法有一個累加器會傳給區塊,累加器是前一次疊代的結果。這會形成一個遞迴的過程,把集合中的所有元素合併起來。
arr = [1, 2, 3]
arr.reduce(0) do |acc, n|
acc + n
end
# => 6
reduce看起來可能和sum很像,但reduce更有彈性,因為它可以讓你指定累加器的初始值和合併方法。reduce可以用來實作sum、count等方法。
在卡牌遊戲 Spellbound Steel 中,你的牌組裡有各種卡牌,上頭可能是不同的角色,例如 warrior、mage 和 rogue,也可能是不同的法術,例如 fireball、ice storm 和 lightning bolt。 遊戲中有二到四位玩家,目標是打出威力值更高的卡牌來擊敗其他玩家。
你想建立各種方法來計算、排序和比較卡牌的威力值,好在遊戲中取得優勢。
你不確定某張卡牌是否在牌組裡。 因此,你想要一個方法,能告訴你牌組中第一張你想找的卡牌在哪裡。 如果牌組裡沒有這張卡牌,你也想知道。
實作一個方法 SpellboundSteel.find_card?,它接受一個由 String 卡牌組成的 Array,以及一張要尋找的 String 卡牌。
這個方法應該回傳牌組中第一張符合要尋找卡牌的索引。
如果牌組裡沒有這張卡牌,方法應該回傳 nil。
cards = ["Warrior", "Mage", "Rogue", "Fireball", "Ice Storm", "Lightning Bolt", "Mage"]
SpellboundSteel.find_card?(cards, "Mage")
# => 1
為了讓卡牌更容易閱讀,你想把卡牌名稱中的所有字元改成大寫。 這樣一來,你打出卡牌時就能輕易看清卡牌名稱。
實作一個方法 SpellboundSteel.capitalize_cards,它接受一個由 String 卡牌組成的 Array,並回傳所有字元都已改成大寫的卡牌。
cards = ["warrior", "mage", "rogue", "fireball", "ice storm", "lightning bolt"]
SpellboundSteel.capitalize_cards(cards)
# => ["Warrior", "Mage", "Rogue", "Fireball", "Ice Storm", "Lightning Bolt"]
在遊戲中,計算所有卡牌的威力值很重要。 所有卡牌的威力值可以在下表中看到:
| 卡牌類型 | 威力值 |
|---|---|
| Warrior | 10 |
| Mage | 20 |
| Rogue | 30 |
| Fireball | 15 |
| Ice Storm | 25 |
| Lightning Bolt | 35 |
實作一個方法 SpellboundSteel.calculate_power_level,它接受一個由 String 卡牌組成的 Array,並回傳所有卡牌的威力值總和。
cards = ["Warrior", "Mage", "Rogue", "Fireball", "Ice Storm", "Lightning Bolt"]
SpellboundSteel.calculate_power_level(cards)
# => 135
遊戲中有所謂的詛咒卡牌。 這些卡牌是隱藏的卡牌,只有打出它的玩家才能解碼。 要解碼卡牌,你需要取出卡牌名稱中每隔一個的字元,並移除其餘的字元。 這樣就會得到卡牌的名稱。
實作一個方法 SpellboundSteel.decode_card,它接受一張 String 卡牌,並回傳解碼後的卡牌。
SpellboundSteel.decode_card("Wbalrrrlimoqr")
# => "Warrior"