【问题标题】:Is there a way inside a loop to know if you're on the final iteration?循环中有没有办法知道你是否在最后一次迭代中?
【发布时间】:2020-01-01 22:14:34
【问题描述】:

在遍历数组(或任何其他可枚举)时,是否有任何简洁明了的方法可以知道您是否处于最后一次迭代中?

例如,在 Rails 视图中,您可能需要显示一组内容,并且您可能希望在它们之间放置一条水平线,但不是在每个之前,也不是在每个之后。只是他们之间。我确信还有许多其他可能的用途,但这是我认为最常见的一种:在集合项目本身的数据之间显示一些东西

我很确定除了将each 切换到each_with_indexeach.with_index 并在迭代之外添加一个变量以获取项目总数,然后在块的末尾检查之外别无他法。

由于似乎非常不太可能有内置方法使用each 执行此操作,是否可以创建一种方法来为您提供此功能而无需在迭代之外添加代码?最简单的形式(这不是我正在寻找的,但可能是朝着正确方向迈出的一步)可能类似于

people.each.with_count.with_index(1) do |person, count, index|
  # ...
  do_something_in_between if index < count
end

但实际上我更喜欢类似的东西

people.each do |person|
  # ...
end.between do
  do_something_in_between
end

有可能实现吗?

【问题讨论】:

  • 当我遇到这种情况时,我pop 将最后一个元素放入一个临时数组或变量中,处理主数组然后处理剩余的元素。我们永远不应该真正关心我们是否在元素 n 上,我们分块处理并循环直到完成。
  • 在集合的数据之间放置一些东西 - 这不是 join 所做的吗?

标签: ruby-on-rails ruby metaprogramming


【解决方案1】:

如果您不想使用计数器或生成的偏移量,您可以执行以下操作。

people = %w|Lily Bob Suzie|
  #=> ["Lily", "Bob", "Suzie"]

enum = people.each.with_index(1)
  #=> #<Enumerator: #<Enumerator: ["Lily", "Bob", "Suzie"]:
  #     each>:with_index(1)> 
loop do
  name, nbr = enum.next
  print "name=#{name}, nbr=#{nbr} is "
  puts (enum.peek rescue nil) ? "not last" : "last"
end

展示

name=Lily, nbr=1 is not last
name=Bob, nbr=2 is not last
name=Suzie, nbr=3 is last

Enumerator#nextEnumerator#peekpeek 返回下一个要生成的元素(如果有),否则会引发 StopInteration 异常(即,如果前一个 next 生成了最后一个元素)。如果peek 引发异常,我使用内联rescue 子句返回nil

为了只挽救由peek 产生的StopInteration 异常(并且还允许people 包含对象falsenil,尽管这与当前上下文无关),我们可以使用@ 987654338@构造:

enum = people.each.with_index(1)
loop do
  print "name=%s, nbr=%d is " % enum.next
  begin
    enum.peek
    puts "not last"
  rescue StopIteration
    puts "last"
  end
end

我们依赖enum.next 来生成StopIteration 异常,当它在枚举器生成所有元素后执行时。 Kernel#loop 通过跳出循环来响应该异常。因此,enum.next 不能在 begin/rescue/end 子句中。

【讨论】:

  • 这非常聪明,我很高兴知道这一点,但就可读性而言,缺点大于优点。所以 +1,但我不认为自己使用它是为了使代码更具可读性并减少可供读者跟踪的细节。
  • 我以为您是在询问是否可以在循环中确定是否正在处理集合的最后一个元素,而无需维护其在集合中的偏移量。我不主张按照我描述的方式做。 (我只需维护一个索引i 并检查i &lt; arr.size-1 传递给块的每个元素。)您的标题询问它是否“可能”,而不是“可能和建议”。您熟悉如何使用索引来完成它,因此您似乎正在寻找一种不维护索引的解决方案。
  • @CarySwoveland :我认为对于一般情况(例如,编写一个通用迭代器,它将一个 bool 传递给块,指示我们是否在最后一次迭代中),您使用 peek 的想法并且捕获StopIteration 比运行索引更好,因为如果集合不是数组,则计算大小可能是一项昂贵的操作。
  • @CarySwoveland:是的,我仍然非常感谢你的回答。
【解决方案2】:

我通常使用外部定义的变量来捕获我是否在第一次迭代中。我不担心最后一次迭代,因为(如果它不是第一次)它需要中间过程,就像所有其他迭代一样。

first_done = false

people.each do |person|
  unless first_done
    # do stuff on all but first
  end
  first_done = true
  # do stuff for current person
end

正确地说,可以说 first_done = true 应该在 else 块中,但我认为这会稍微增加下一位维护者的理解复杂性。

您的 between 示例无法工作,因为来自 each 迭代的返回值是迭代的集合...当评估 between 时,each 将完成。

您可以通过封装在方法中的上述技术,传递一个 lambda 来做到这一点...下面的示例是一个外部方法,但它可以是一个数组方法,您可以对其进行修补。

不管怎样,外部方法...

inbetween_stuff = -> (person) do
  # inbetween actions, optionally reference person
end

inbetween(people, inbetween_stuff) do |person|
  # normal record processing
end

def inbetween(collection, pre_action) do
  first_time = false
    collection.each do |item|
    unless first_time
      pre_action.call(item)
    end
    first_time = true
    yield(item)
  end
end

【讨论】:

    【解决方案3】:

    你可以定义自己的辅助类

    class Iconoclast
      class Config
        attr_reader :handle_first, :handle_last, :handle_between
    
        def first(&block)
          @handle_first = block
        end
    
        def last(&block)
          @handle_last = block
        end
    
        def between(&block)
          @handle_between = block
        end
      end
    
      def self.iterate(collection)
        config = Config.new
        yield(config)
        return if collection.empty?
    
        config.handle_first.call(collection.first) if config.handle_first
        collection.each_with_index do |item, index|
          config.handle_between.call(item) if index > 0 && index < collection.size - 1
        end if config.handle_between
        config.handle_last.call(collection.last) if config.handle_last
      end
    end
    
    
    Iconoclast.iterate([1, 2, 3, 4, 5]) do |config|
      config.first { |item| puts "The first Item is #{item}" }
      config.between { |item| puts "Neither first nor last #{item}"}
      config.last { |item| puts "The last Item is #{item}" }
    end
    

    您也可以将此方法monkeypatch 到Enumerable。那你就可以了

    [1, 2, 3, 4, 5].iterate do |config|
      config.first { |item| puts "The first Item is #{item}" }
      config.between { |item| puts "Neither first nor last #{item}"}
      config.last { |item| puts "The last Item is #{item}" }
    end
    

    但我不喜欢猴子补丁,所以这只是为了完整性而提及:-)

    【讨论】:

    • 我可以使用改进而不是直接猴子补丁...这种方法看起来很有希望...谢谢!
    【解决方案4】:

    根据 Sebastians 的回答,我们知道最后一项位于索引 -1,如果您想对除最后一次迭代之外的所有内容执行操作:

    people.each_with_index |person, index| do
      do_something unless index == -1
    end
    

    【讨论】:

    • index 从零开始增加到people.size - 1;它永远不是-1。如问题中所述,将-1 替换为people.size - 1 有效,但这不是OP 想要的。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-12-15
    • 2013-03-18
    • 2019-01-23
    • 2021-11-17
    • 1970-01-01
    相关资源
    最近更新 更多