在编程中,迭代指的是重复执行一段代码块的过程。这可以通过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 中,你有一副卡牌,牌面要么是不同的角色,比如战士、法师和盗贼,要么是不同的法术,比如火球、冰风暴和闪电箭。 游戏有两到四名玩家,目标是通过打出能量等级更高的卡牌来击败其他玩家。
你想通过编写各种方法来计算、排序和比较卡牌的能量等级,从而在游戏中占据上风。
你不确定某张卡牌是否在牌组里。因此,你想要一个方法,告诉你能在牌组中哪里找到你想要的那种卡牌中的第一张。如果牌组里没有这张卡牌,你也希望知道这一点。
实现一个方法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"]
在游戏中,计算所有卡牌的能量等级很重要。所有卡牌的能量可以在下表中看到:
| Card Type | Power Level |
|---|---|
| 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"