MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Ruby循环结构及迭代器使用技巧

2021-12-217.4k 阅读

Ruby 循环结构基础

在 Ruby 编程中,循环结构是一种重要的控制结构,它允许我们重复执行一段代码。Ruby 提供了多种循环结构,每种都有其独特的用途和特点。

while 循环

while 循环是最基本的循环结构之一,它会在给定条件为 true 时重复执行一段代码块。其语法如下:

while condition
  # 要执行的代码块
end

例如,我们要打印从 1 到 5 的数字,可以这样写:

i = 1
while i <= 5
  puts i
  i += 1
end

在这个例子中,i <= 5 是条件,只要这个条件为 true,就会执行 puts ii += 1 这两行代码。每次循环结束后,i 的值会增加 1,直到 i 大于 5 时,条件为 false,循环结束。

until 循环

until 循环与 while 循环相反,它会在给定条件为 false 时重复执行代码块。语法如下:

until condition
  # 要执行的代码块
end

同样是打印从 1 到 5 的数字,用 until 循环可以写成:

i = 1
until i > 5
  puts i
  i += 1
end

这里 i > 5 是条件,当这个条件为 false 时,也就是 i 小于等于 5 时,会执行代码块。随着 i 的不断增加,当 i 大于 5 时,条件变为 true,循环结束。

for 循环

for 循环用于遍历一个范围或一个可枚举对象。语法如下:

for variable in range_or_enumerable
  # 要执行的代码块
end

比如,遍历一个范围:

for i in 1..5
  puts i
end

这里 1..5 是一个范围,表示从 1 到 5(包括 1 和 5)。for 循环会依次将 i 赋值为范围内的每个值,并执行代码块。

遍历一个数组也很简单:

fruits = ["apple", "banana", "cherry"]
for fruit in fruits
  puts fruit
end

在这个例子中,for 循环会依次将 fruit 赋值为数组 fruits 中的每个元素,并执行 puts fruit 这行代码,从而打印出每个水果的名称。

循环控制语句

在循环执行过程中,我们有时需要更精细地控制循环的流程,这就用到了循环控制语句。

break 语句

break 语句用于立即终止当前循环,跳出循环体。例如:

i = 1
while i <= 10
  if i == 5
    break
  end
  puts i
  i += 1
end

在这个 while 循环中,当 i 的值等于 5 时,执行 break 语句,循环立即终止,后面的 puts ii += 1 不会再执行,因此只会打印出 1 到 4。

for 循环中同样适用:

for i in 1..10
  if i == 7
    break
  end
  puts i
end

这里当 i 等于 7 时,break 语句使 for 循环终止,只打印出 1 到 6。

next 语句

next 语句用于跳过当前循环的剩余部分,直接进入下一次循环。例如:

i = 1
while i <= 10
  if i == 3
    i += 1
    next
  end
  puts i
  i += 1
end

在这个 while 循环中,当 i 等于 3 时,执行 next 语句,跳过 puts i 这一行,直接执行 i += 1,然后进入下一次循环。所以输出结果中不会有 3。

for 循环中:

for i in 1..10
  if i.even?
    next
  end
  puts i
end

这里如果 i 是偶数,执行 next 语句,跳过 puts i,直接进入下一次循环,因此只会打印出 1 到 10 中的奇数。

redo 语句

redo 语句用于重新开始当前循环,而不检查条件(对于 whileuntil 循环)或不移动到下一个元素(对于 for 循环)。例如:

i = 1
while i <= 5
  if i == 3
    i += 1
    redo
  end
  puts i
  i += 1
end

i 等于 3 时,执行 redo 语句,循环会回到开始处,重新执行 puts ii += 1,而不会重新检查 i <= 5 这个条件。所以输出结果会是 1、2,然后因为 redo 会不断重复输出 4,直到手动终止程序。

for 循环中:

for i in 1..5
  if i == 3
    redo
  end
  puts i
end

这里当 i 等于 3 时,执行 redo 语句,会重新执行 puts i,而不会移动到下一个元素 4,所以也会陷入类似的重复输出状态。

Ruby 迭代器

迭代器是 Ruby 中一种强大的功能,它允许我们以一种更简洁、更灵活的方式遍历可枚举对象。

each 迭代器

each 是最常用的迭代器之一,它会对可枚举对象(如数组、哈希等)的每个元素执行给定的代码块。对于数组:

fruits = ["apple", "banana", "cherry"]
fruits.each do |fruit|
  puts fruit
end

这里 fruits.each 会依次将数组 fruits 中的每个元素传递给代码块,并将元素赋值给 fruit 变量,然后执行 puts fruit 这行代码,从而打印出每个水果的名称。

对于哈希:

person = {name: "Alice", age: 30}
person.each do |key, value|
  puts "#{key}: #{value}"
end

在这个哈希的 each 迭代中,每次迭代会将哈希的键赋值给 key,值赋值给 value,然后执行代码块中的 puts 语句,输出键值对。

map 迭代器

map 迭代器会对可枚举对象的每个元素执行代码块,并返回一个新的数组,新数组的元素是代码块的返回值。例如:

numbers = [1, 2, 3, 4]
squared_numbers = numbers.map do |num|
  num ** 2
end
puts squared_numbers

这里 numbers.map 会对数组 numbers 中的每个元素执行 num ** 2 这个代码块,返回每个元素的平方值,然后将这些平方值组成一个新的数组 squared_numbers。最后打印出 [1, 4, 9, 16]

select 迭代器

select 迭代器用于从可枚举对象中选择满足特定条件的元素,并返回一个新的数组。例如:

numbers = [1, 2, 3, 4, 5, 6]
even_numbers = numbers.select do |num|
  num.even?
end
puts even_numbers

这里 numbers.select 会检查数组 numbers 中的每个元素,只有当元素满足 num.even?(即元素是偶数)这个条件时,才会被选入新的数组 even_numbers。最终打印出 [2, 4, 6]

reject 迭代器

reject 迭代器与 select 迭代器相反,它会从可枚举对象中排除满足特定条件的元素,返回一个新的数组。例如:

numbers = [1, 2, 3, 4, 5, 6]
odd_numbers = numbers.reject do |num|
  num.even?
end
puts odd_numbers

这里 numbers.reject 会检查数组 numbers 中的每个元素,当元素满足 num.even? 这个条件时,该元素会被排除,最终返回一个只包含奇数的新数组 odd_numbers,打印出 [1, 3, 5]

find 迭代器

find 迭代器用于在可枚举对象中查找满足特定条件的第一个元素。例如:

numbers = [1, 2, 3, 4, 5, 6]
found_number = numbers.find do |num|
  num > 3
end
puts found_number

这里 numbers.find 会依次检查数组 numbers 中的每个元素,当找到第一个满足 num > 3 条件的元素时,就会返回该元素,即 4。如果没有找到满足条件的元素,find 会返回 nil

detect 迭代器

detect 迭代器与 find 迭代器功能相同,是 find 的别名。例如:

numbers = [1, 2, 3, 4, 5, 6]
found_number = numbers.detect do |num|
  num.even? && num > 4
end
puts found_number

这里 numbers.detect 会查找数组 numbers 中第一个满足 num.even? && num > 4 条件的元素,即 6,并返回它。

all? 迭代器

all? 迭代器用于检查可枚举对象的所有元素是否都满足特定条件。例如:

numbers = [2, 4, 6, 8]
all_even = numbers.all? do |num|
  num.even?
end
puts all_even

这里 numbers.all? 会检查数组 numbers 中的每个元素是否都是偶数,由于所有元素都是偶数,所以 all_eventrue,最终打印出 true。如果数组中有一个元素不满足条件,all? 就会返回 false

any? 迭代器

any? 迭代器用于检查可枚举对象中是否有任何一个元素满足特定条件。例如:

numbers = [1, 3, 5, 7]
any_even = numbers.any? do |num|
  num.even?
end
puts any_even

这里 numbers.any? 会检查数组 numbers 中是否有偶数,由于数组中没有偶数,所以 any_evenfalse,最终打印出 false。只要数组中有一个元素满足条件,any? 就会返回 true

none? 迭代器

none? 迭代器与 any? 迭代器相反,它用于检查可枚举对象中是否没有任何一个元素满足特定条件。例如:

numbers = [1, 3, 5, 7]
none_even = numbers.none? do |num|
  num.even?
end
puts none_even

这里 numbers.none? 会检查数组 numbers 中是否没有偶数,因为数组中确实没有偶数,所以 none_eventrue,最终打印出 true。如果数组中有一个元素满足条件,none? 就会返回 false

嵌套循环与迭代器

在 Ruby 中,我们可以将循环和迭代器进行嵌套,以实现更复杂的逻辑。

嵌套循环

例如,我们要打印一个乘法表,可以使用嵌套的 for 循环:

for i in 1..9
  for j in 1..9
    print "#{i} * #{j} = #{i * j}\t"
  end
  puts
end

这里外层的 for 循环控制行数,内层的 for 循环控制列数。每次外层循环的 i 固定时,内层循环会遍历 1 到 9 的 j,计算并打印出乘法表的每一项。print 方法用于打印内容但不换行,puts 方法用于换行,从而形成一个整齐的乘法表格式。

嵌套迭代器

我们也可以使用嵌套迭代器来实现类似的功能。比如,用 each 迭代器打印一个简单的矩阵:

matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
matrix.each do |row|
  row.each do |element|
    print "#{element}\t"
  end
  puts
end

这里外层的 matrix.each 迭代器遍历矩阵的每一行,将每一行赋值给 row。内层的 row.each 迭代器遍历每一行中的每个元素,将元素赋值给 element,然后打印出来。同样,print 用于不换行打印,puts 用于换行,从而打印出矩阵的格式。

循环与迭代器的性能考虑

在实际编程中,性能是一个重要的考虑因素。不同的循环结构和迭代器在性能上可能会有差异。

循环结构的性能

一般来说,whileuntil 循环在简单的计数循环场景下,如果代码逻辑不复杂,性能表现较好。因为它们的结构简单,只涉及条件判断和代码块执行。例如:

start_time = Time.now
i = 0
while i < 1000000
  i += 1
end
end_time = Time.now
puts "While loop took #{(end_time - start_time).to_f} seconds"

for 循环在遍历范围时性能也不错,因为它的实现相对直接。但在遍历复杂的可枚举对象时,由于它需要创建迭代器对象等额外操作,性能可能会稍逊一筹。

迭代器的性能

each 迭代器是最常用的迭代器之一,它的性能通常比较好,因为它是基于底层的 C 实现,直接遍历可枚举对象。例如:

start_time = Time.now
array = (1..1000000).to_a
array.each do |num|
  num * 2
end
end_time = Time.now
puts "Each iterator took #{(end_time - start_time).to_f} seconds"

map 迭代器在生成新数组时,由于需要对每个元素执行代码块并创建新的数组,性能会比 each 略差一些,特别是当代码块执行的操作比较复杂时。

selectreject 迭代器的性能取决于可枚举对象的大小和条件判断的复杂度。如果对象很大且条件判断复杂,性能会受到影响。

finddetect 迭代器在找到满足条件的第一个元素后就会停止迭代,所以如果可枚举对象很大且满足条件的元素靠前,性能会比较好;如果靠后或者不存在,可能会遍历整个对象,性能就会下降。

all?any?none? 迭代器通常性能较好,因为它们在找到结果后就会停止迭代。例如,all? 一旦发现有一个元素不满足条件就会返回 falseany? 一旦发现有一个元素满足条件就会返回 true

在实际应用中,我们需要根据具体的需求和数据规模来选择合适的循环结构和迭代器,以达到最佳的性能。

循环和迭代器在实际项目中的应用

在实际的 Ruby 项目中,循环和迭代器无处不在。

数据处理

在数据处理场景中,经常需要遍历数组或哈希等数据结构。例如,在一个电商项目中,有一个订单数组,每个订单是一个哈希,包含订单号、客户信息、商品列表等。我们可能需要遍历订单数组,统计每个客户的订单数量:

orders = [
  {order_id: 1, customer: "Alice", items: ["book", "pen"]},
  {order_id: 2, customer: "Bob", items: ["shirt"]},
  {order_id: 3, customer: "Alice", items: ["shoes"]}
]
customer_order_count = {}
orders.each do |order|
  customer = order[:customer]
  if customer_order_count.key?(customer)
    customer_order_count[customer] += 1
  else
    customer_order_count[customer] = 1
  end
end
puts customer_order_count

这里通过 each 迭代器遍历订单数组,对每个订单中的客户进行统计,最终得到每个客户的订单数量。

文件操作

在文件操作中,也会用到循环和迭代器。比如,我们要读取一个文本文件,统计文件中每行的单词数量:

line_word_count = []
File.foreach('example.txt') do |line|
  words = line.split
  line_word_count << words.length
end
puts line_word_count

这里 File.foreach 迭代器逐行读取文件内容,通过 split 方法将每行拆分成单词数组,统计单词数量并添加到 line_word_count 数组中。

算法实现

在算法实现中,循环和迭代器更是必不可少。例如,实现冒泡排序算法:

def bubble_sort(array)
  length = array.length
  loop do
    swapped = false
    (length - 1).times do |i|
      if array[i] > array[i + 1]
        array[i], array[i + 1] = array[i + 1], array[i]
        swapped = true
      end
    end
    break unless swapped
  end
  array
end
array = [5, 4, 3, 2, 1]
puts bubble_sort(array)

这里通过 loop 循环和 times 迭代器实现了冒泡排序算法,不断比较相邻元素并交换位置,直到数组有序。

通过以上对 Ruby 循环结构及迭代器的详细介绍,包括基础语法、控制语句、各种迭代器的使用、嵌套应用、性能考虑以及实际项目中的应用,相信你对它们有了更深入的理解,可以在 Ruby 编程中更加灵活和高效地运用它们。