【问题标题】:Identifying last loop when using for each使用 for each 时识别最后一个循环
【发布时间】:2010-11-07 06:36:09
【问题描述】:

在对对象执行“foreach”时,我想对最后一次循环迭代做一些不同的事情。我使用的是 Ruby,但 C#、Java 等也是如此。

  list = ['A','B','C']
  list.each{|i|
    puts "Looping: "+i # if not last loop iteration
    puts "Last one: "+i # if last loop iteration
  }

所需的输出相当于:

  Looping: 'A'
  Looping: 'B'
  Last one: 'C'

显而易见的解决方法是使用 'for i in 1..list.length' 将代码迁移到 for 循环,但 for each 解决方案感觉更优雅。在循环期间编写特殊情况的最优雅的方法是什么?用foreach能做到吗?

【问题讨论】:

  • 感谢所有伟大而多样的答案。我当然明白人们对收藏品并不总是有订单的看法。但有时集合确实有顺序,我不明白为什么 for each 循环不应该仍然适用。
  • 对不起,我只能选择一个答案,我喜欢其中的大多数。
  • @MattChurchy,可惜你没有选择更好的。

标签: c# java ruby loops foreach


【解决方案1】:

Foreach 很优雅,因为它不关心列表中的项目数量并且平等对待每个元素,我认为你唯一的解决方案是使用一个 for 循环,它要么在 itemcount-1 处停止,然后你呈现你的最后一个循环外的项目或处理该特定条件的循环内的条件,即 if (i==itemcount) { ... } else { ... }

【讨论】:

  • 嗯,我想知道“itemcount”是否在循环中可用。因为是foreach循环,所以没有计数,只知道当前值,高级是不知道的。所以我在 if 语句中没有什么可比较的。
  • 哦,没关系,你确实说过'for'循环。遗憾的是,在 foreach 循环中,我无法获得列表中的当前位置。
  • 如果列表中的位置对您很重要,那么索引 for 循环 优雅的解决方案,仅仅因为它很常见且经常被误用并不会使它变得不优雅。
【解决方案2】:

至少在 C# 中,如果没有常规的 for 循环,这是不可能的。

集合的枚举器决定下一个元素是否存在(MoveNext 方法),循环不知道这一点。

【讨论】:

    【解决方案3】:

    这够优雅吗?它假定一个非空列表。

      list[0,list.length-1].each{|i|
        puts "Looping:"+i # if not last loop iteration
      }
      puts "Last one:" + list[list.length-1]
    

    【讨论】:

    • 显然有人反对,但我喜欢这个建议,它是跳出框框思考,肯定会做的。
    • 不错。您也可以分别使用 list[0...-1] 和 list[-1] 代替 list[0, list.length-1] 和 list[list.length-1]。
    • 请注意,Ruby 数组中的最后一行也是 list.last,这可能会使最后一行更加清晰。
    • 好主意。但是这种方法不能应用于哈希,可以吗?
    • 这是我最喜欢的(在精神上):它的好处是在每次迭代时不进行不必要的 if 检查
    【解决方案4】:

    只有在处理 each 一个相同的情况下,才应该使用 foreach。改用基于索引的交互。否则,您必须在项目周围添加不​​同的结构,您可以使用它来区分 foreach 调用中的正常和最后一个(查看关于从 google 减少的地图作为背景的好论文:http://labs.google.com/papers/mapreduce.html,map == foreach,减少 == 例如总和或过滤器)。

    Map 不知道结构(尤其是 item 在哪个位置),它只能逐个转换一个 item(没有一个 item 的知识可以用来转换另一个 item!),但是 reduce 可以使用内存来转换例如计算位置并处理最后一项。

    一个常见的技巧是反转列表并处理第一个(现在已知索引 = 0),然后再次应用反转。 (优雅但不快速;))

    【讨论】:

    • 在许多情况下,您不能使用基于索引的迭代,因为并非所有类型的集合都支持它。例如,以 IEnumerable 接口 (.NET) 为例:您可以枚举项目,但不能通过索引访问它们……您甚至不知道枚举中有多少项目。所以在这种情况下,foreach 是唯一的方法......
    【解决方案5】:

    foreach 结构(在 Java 中肯定,也可能在其他语言中)旨在表示最通用的 if 迭代,其中包括对没有有意义的迭代顺序的集合的迭代。例如,基于散列的集合没有排序,因此 没有“最后一个元素”。每次迭代时,最后一次迭代可能会产生不同的元素。

    基本上:不,foreach 构造不应该以这种方式使用。

    【讨论】:

    • +1 让我感到困惑的是,上面的 .Length/.Count 答案得到了这么多的选票
    • 在最后一个元素上做一些不同的事情并不意味着你希望一个特定的元素成为最后一个。例如,如果您有一个字符串列表,您需要将其连接成一个带有分隔符的大字符串。您将循环每个字符串,连接字符串,然后连接分隔符。在最后一个字符串上,您不希望添加分隔符。因此,即使我同意为最后一个元素做一些特定的事情会破坏“foreach”背后的逻辑,但最后一个元素并不总是相同的事实完全无关紧要。
    • 虽然 foreach 语句只需要 Iterator 模式而不需要特定的顺序,但这并不意味着它对有序集合没有用处。事实上,它比经典的 for with index 循环更好,因为它可以避免出现越界错误。
    • @Ksempac - 很公平,无论如何我都对写这些感到内疚:)
    • 这种“没有解决方案的否认”类型的答案有名称吗?我不是他们的粉丝。大卫伯罗斯有正确的答案。
    【解决方案6】:

    如何首先获得对last itemreference,然后在foreach循环中使用它进行比较? 我并不是说你应该这样做,因为我自己会使用 KlauseMeier 提到的基于索引的循环。抱歉,我不了解 Ruby,所以以下示例使用 C#!希望你不介意:-)

    string lastItem = list[list.Count - 1];
    foreach (string item in list) {
        if (item != lastItem)
            Console.WriteLine("Looping: " + item);
        else    Console.Writeline("Lastone: " + item);
    }
    

    我修改了下面的代码,通过引用而不是值进行比较(只能使用引用类型而不是值类型)。以下代码应该支持包含相同字符串(但不是相同字符串对象)的多个对象,因为 MattChurcy 的示例没有指定字符串必须是不同的,并且我使用 LINQ Last 方法而不是计算索引。

    string lastItem = list.Last();
    foreach (string item in list) {
        if (!object.ReferenceEquals(item, lastItem))
            Console.WriteLine("Looping: " + item);
        else        Console.WriteLine("Lastone: " + item);
    }
    

    上述代码的限制。 (1) 它只能用于字符串或引用类型而不是值类型。 (2) 同一个对象在列表中只能出现一次。您可以拥有包含相同内容的不同对象。文字字符串不能重复使用,因为 C# 不会为具有相同内容的字符串创建唯一对象。

    我不傻。我知道基于索引的循环是可以使用的。当我第一次发布最初的答案时,我已经说过了。我在问题的context 中提供了最佳答案。我太累了,无法继续解释这一点,所以你们都可以投票删除我的答案。如果这个人走了,我会很高兴的。谢谢

    【讨论】:

    • C# 3.0 也将在数组上有 .Last 运算符
    • @Kirchstein:如果集合没有实现 IList,那么调用 Last 方法将涉及评估整个序列,因此性能可能很差。如果集合确实实现了 IList,那么您可以像上面的 TomatoGG 那样执行“var lastItem = list[list.Count - 1]”之类的操作。
    • 这是一个容易出错的代码。您实际上是按值比较,而不是按索引。如果有一个对象等于最后一个对象,您将有几个“lastone:”行。
    • 哇,这个 stackoverflow 的东西真的很难。真的不知道我应该如何处理一个问题。我应该尝试根据提问者的需求提供解决方案,还是通过拒绝提供不符合标准的答案来成为标准和最佳实践的传播者?
    • @TomatoGG:要使其正常工作,您需要按索引查找最后一项,not 按值查找,not 按引用查找。这就是为什么如果集合不能通过索引访问,那么使用“foreach”就不能真正获得优雅的解决方案,如果集合可以通过索引访问,那么简单的“for”循环将提供比“foreach”更优雅的解决方案。
    【解决方案7】:

    你可以做这样的事情(C#):

    string previous = null;
    foreach(string item in list)
    {
        if (previous != null)
            Console.WriteLine("Looping : {0}", previous);
        previous = item;
    }
    if (previous != null)
        Console.WriteLine("Last one : {0}", previous);
    

    【讨论】:

    • 这个答案有什么问题?不加解释的投票真的没用……
    • 再次,这与其他答案不同,这很棒。我喜欢多样化的答案。但是我不确定这会打印列表中的所有项目。
    • @MattChurchy, @TomatoGG:它将打印列表中的所有非空项目,但如果列表包含任何空值,它就会下降。
    • 我仍然不确定如果不编译会做什么。但是谢谢你的回答。我认为也许难以解释结果会将其排除在优雅的解决方案之外。
    • 经过测试,确实有效。我终于和你在一起了。 'previous' 变量不是标志,而是保存上一个循环的结果。将列表中的最后一个变量推到循环外。 - 再次感谢您的回答
    【解决方案8】:

    您可以在您的类中定义一个eachwithlast 方法,对除最后一个元素之外的所有元素执行与each 相同的操作,但对最后一个元素执行其他操作:

    class MyColl
      def eachwithlast
        for i in 0...(size-1)
          yield(self[i], false)
        end
        yield(self[size-1], true)
      end
    end
    

    那么你可以这样称呼它(fooMyColl 的一个实例或其子类):

    foo.eachwithlast do |value, last|
      if last
        puts "Last one: "+value
      else
        puts "Looping: "+value
      end
    end
    

    编辑: 按照 molf 的建议:

    class MyColl
      def eachwithlast (defaultAction, lastAction)
        for i in 0...(size-1)
          defaultAction.call(self[i])
        end
        lastAction.call(self[size-1])
      end
    end
    
    foo.eachwithlast(
        lambda { |x| puts "looping "+x },
        lambda { |x| puts "last "+x } )
    

    【讨论】:

    • 绝对喜欢那个。但我不确定我是否要为这个简单的任务扩展内置的 Array 类。
    • 我认为这种不情愿是没有道理的。我猜它来自于方法“属于”一个类的概念,而不是泛型函数。我认为如果你想要数组的新行为,你必须尽其所能。
    • 如果可以避免块内的条件,那就更好了。如果你让 eachwithlast 使用两个 lambda,一个用于最多 n-1 的项目,一个用于项目 n,我认为会更友好一些。
    • 感谢您的回答 Svante 和 molf。更惯用的 ruby​​ 代码可能会使用 each_with_index,而 ruby​​ 通常使用 snake_case 作为变量和方法名。就像这里无耻地抄袭代码一样:stackoverflow.com/questions/2241684/…
    【解决方案9】:

    Ruby 也有 each_index 方法:

    list = ['A','B','C']
    list.each_index{|i|
      if i < list.size - 1
        puts "Looping:"+list[i]
      else
        puts "Last one:"+list[i]
    }
    

    编辑:

    或使用每个(校正后的 TomatoGG 和 Kirschstein 解决方案):

    list = ['A', 'B', 'C', 'A']
    list.each { |i|
       if (i.object_id != list.last.object_id)
          puts "Looping:#{i}"
       else
          puts "Last one:#{i}"
       end
    }
    
    Looping:A
    Looping:B
    Looping:C
    Last one:A
    

    或者

    list = ['A', 'B', 'C', 'A']
    list.each {|i| 
      i.object_id != list.last.object_id ? puts "Looping:#{i}" : puts "Last one:#{i}"       
    }
    

    【讨论】:

    • 好东西,如果我知道它在那里,我现在可能已经用过几次了。干杯
    【解决方案10】:

    我认为我更喜欢 kgiannakakis 的解决方案,但是您总是可以这样做;

    list = ['A','B','C']
    list.each { |i|
       if (i != list.last)
          puts "Looping:#{i}"
       else
          puts "Last one:#{i}"
       end
    }
    

    【讨论】:

    • 谢谢,这似乎是 TomatoGG 回答的更严格的语法
    • 你应该在最后一个 puts 之后添加“end”,如果你有 list = ['A', 'B', 'C', 'A'] 这个解决方案不起作用 - 它会打印“最后一个:A”两次。
    【解决方案11】:

    如果您使用的集合公开了 Count 属性 - 许多其他答案做出的假设,所以我也会这样做 - 那么你可以做这样的事情使用 C# 和 LINQ:

    foreach (var item in list.Select((x, i) => new { Val = x, Pos = i }))
    {
        Console.Write(item.Pos == (list.Count - 1) ? "Last one: " : "Looping: ");
        Console.WriteLine(item.Val);
    }
    

    如果我们另外假设集合中的项目可以通过索引直接访问 - the currently accepted answer 假设这一点 - 那么普通的 for 循环将比 @987654327 更优雅/可读@:

    for (int i = 0; i < list.Count; i++)
    {
        Console.Write(i == (list.Count - 1) ? "Last one: " : "Looping: ");
        Console.WriteLine(list[i]);
    }
    

    如果集合没有公开Count 属性并且不能通过索引访问,那么实际上没有任何优雅的方法可以做到这一点,至少在C# 中没有。 Thomas Levesque's answer 的错误修复变体可能与您将得到的一样接近。


    这是Thomas's answer的错误修复版本:

    string previous = null;
    bool isFirst = true;
    foreach (var item in list)
    {
        if (!isFirst)
        {
            Console.WriteLine("Looping: " + previous);
        }
        previous = item;
        isFirst = false;
    }
    if (!isFirst)
    {
        Console.WriteLine("Last one: " + previous);
    }
    

    如果集合不公开Count 属性并且项目不能通过索引直接访问,我将在 C# 中执行此操作。 (请注意,没有 foreach 并且代码不是特别简洁,但它几乎可以在任何可枚举的集合中提供不错的性能。)

    // i'm assuming the non-generic IEnumerable in this code
    // wrap the enumerator in a "using" block if dealing with IEnumerable<T>
    var e = list.GetEnumerator();
    if (e.MoveNext())
    {
        var item = e.Current;
    
        while (e.MoveNext())
        {
            Console.WriteLine("Looping: " + item);
            item = e.Current;
        }
        Console.WriteLine("Last one: " + item);
    }
    

    【讨论】:

      【解决方案12】:

      &lt;hello&gt;我们在想什么?

      public static void main(String[] args) {
          // TODO Auto-generated method stub
          String str = readIndex();
          String comp[] = str.split("}");
      
          StringBuffer sb = new StringBuffer();
          for (String s : comp) {
              sb.append(s);
              sb.append("}\n");
          }
          System.out.println (sb.toString());
      }
      

      作为一种建模符号,OMT 符号的影响占主导地位(例如,对类和对象使用矩形)。尽管 Booch 的“云”符号已被删除,但 Booch 指定较低级别设计细节的能力被接受了。 Objectory 和component notation from Booch 的用例符号与其余符号集成在一起,但语义集成在 UML 1.1 中相对较弱,直到 UML 2.0 主要修订版才真正修复。

      【讨论】:

      • 哈哈,看看 Animesh 的其他答案。完全偏离主题。也许是机器人?
      【解决方案13】:

      类似于 kgiannakakis 的回答:

      list.first(list.size - 1).each { |i| puts "Looping: " + i }
      puts "Last one: " + list.last
      

      【讨论】:

        【解决方案14】:

        在 Ruby 中,我会在这种情况下使用 each_with_index

        list = ['A','B','C']
        last = list.length-1
        list.each_with_index{|i,index|
          if index == last 
            puts "Last one: "+i 
          else 
            puts "Looping: "+i # if not last loop iteration
          end
        }
        

        【讨论】:

        • 谢谢,我不知道这种方法,以后肯定会用到。
        【解决方案15】:

        C# 3.0 或更新版本

        首先,我会写一个扩展方法:

        public static void ForEachEx<T>(this IEnumerable<T> s, Action<T, bool> act)
        {
            IEnumerator<T> curr = s.GetEnumerator();
        
            if (curr.MoveNext())
            {
                bool last;
        
                while (true)
                {
                    T item = curr.Current;
                    last = !curr.MoveNext();
        
                    act(item, last);
        
                    if (last)
                        break;
                }
            }
        }
        

        那么使用新的foreach就很简单了:

        int[] lData = new int[] { 1, 2, 3, 5, -1};
        
        void Run()
        {
            lData.ForEachEx((el, last) =>
            {
                if (last)
                    Console.Write("last one: ");
                Console.WriteLine(el);
            });
        }
        

        【讨论】:

        • 很优雅,我喜欢!但是我更喜欢'while(!last)'而不是显式中断......并且需要初始化'last'
        • 这是我见过的第一个优雅的解决方案,它适用于 Iterable s 而不是 List s。 +1。
        【解决方案16】:

        对于 foreach 循环,您尝试做的似乎有点太高级了。但是,您可以显式使用Iterators。例如,在 Java 中,我会这样写:

        Collection<String> ss = Arrays.asList("A","B","C");
        Iterator<String> it = ss.iterator();
        while (it.hasNext()) {
            String s = it.next();
            if(it.hasNext())
                System.out.println("Looping: " + s);
            else 
                System.out.println("Last one: " + s);
        }
        

        【讨论】:

          【解决方案17】:

          这个怎么样?刚学了一点Ruby。呵呵

          list.collect {|x|(x!=list.last ? "Looping:"+x:"Lastone:"+x) }.each{|i|puts i}      
          

          【讨论】:

          • 好主意,但您是按值比较,所以像 ['A', 'B', 'C', 'D', 'C'] 这样的列表将得到最后两个。也许如果你使用 !x.equal?(list.last)
          • 是的,我知道我需要比较引用而不是值,但我还不知道如何在 Ruby 中做到这一点。这么平等?是为了比较参考文献吗?
          • 我喜欢简洁,我碰巧有一个真实的世界使用,我知道数组值是唯一的。 (这实际上是用于测试代码而不是生产使用。)所以对我来说这很好。虽然它并不严格需要单独收集和打印。
          【解决方案18】:

          我注意到一些建议假设您可以在开始循环之前找到列表中的最后一项,然后将每个项目与该项目进行比较。如果你能有效地做到这一点,那么底层数据结构很可能是一个简单的数组。如果是这样的话,为什么还要麻烦 foreach 呢?随便写:

          for (int x=0;x<list.size()-1;++x)
          {
            System.out.println("Looping: "+list.get(x));
          }
          System.out.println("Last one: "+list.get(list.size()-1));
          

          如果您不能有效地从任意位置检索项目——就像它的底层结构是一个链表——那么获取最后一个项目可能涉及对整个列表的顺序搜索。根据列表的大小,这可能是一个性能问题。如果这是一个经常执行的函数,您可能需要考虑使用数组或 ArrayList 或类似结构,这样您就可以这样做。

          听起来你在问,“用锤子拧螺丝的最佳方法是什么?”,当然更好的问题是,“什么是正确的工具来拧螺丝? ?”

          【讨论】:

            【解决方案19】:

            对于您的案例来说,在每次运行“一般”之前从数组中取出第一个/最后一个元素是否是一个可行的解决方案?

            像这样:

            list = ['A','B','C','D']
            first = list.shift
            last = list.pop
            
            puts "First one: #{first}"
            list.each{|i|
              puts "Looping: "+i
            }
            puts "Last one: #{last}"
            

            【讨论】:

              【解决方案20】:

              这个问题可以通过在 F# 等函数式编程语言中使用模式匹配以优雅的方式解决:

              let rec printList (ls:string list) = 
                  match ls with
                      | [last] -> "Last " + last
                      | head::rest -> "Looping " + head + "\n" + printList (rest)
                      | [] -> ""
              

              【讨论】:

                【解决方案21】:

                我不知道 for-each 循环在除 java 之外的其他语言中是如何工作的。 在 java 中,for-each 使用 for-each 使用的 Iterable 接口来获取 Iterator 并使用它循环。 Iterator 有一个 hasNext 方法,如果您可以在循环中看到迭代器,则可以使用该方法。 实际上,您可以通过将已经获得的 Iterator 包含在 Iterable 对象中来实现这一点,这样 for 循环就可以得到它需要的东西,并且您可以在循环中获得一个 hasNext 方法。

                List<X> list = ...
                
                final Iterator<X> it = list.iterator();
                Iterable<X> itw = new Iterable<X>(){
                  public Iterator<X> iterator () {
                    return it;
                  }
                }
                
                for (X x: itw) {
                  doSomething(x);
                  if (!it.hasNext()) {
                    doSomethingElse(x);
                  }
                }
                

                您可以创建一个包装所有这些可迭代和迭代器内容的类,因此代码如下所示:

                IterableIterator<X> itt = new IterableIterator<X>(list);
                for (X x: itit) {
                  doSomething(x);
                  if (!itit.hasNext()) {
                    doSomethingElse(x);
                  }
                }
                

                【讨论】:

                  【解决方案22】:

                  我在这里看到了很多复杂、难以阅读的代码......为什么不保持简单:

                  var count = list.Length;
                  
                  foreach(var item in list)
                      if (--count > 0)
                          Console.WriteLine("Looping: " + item);
                      else
                          Console.Writeline("Lastone: " + item);
                  

                  这只是一个额外的声明!

                  另一种常见的情况是你想对最后一项做一些额外或更少的事情,比如在项目之间放置一个分隔符:

                  var count = list.Length;
                  
                  foreach(var item in list)
                  {
                      Console.Write(item);
                      if (--count > 0)
                          Console.Write(",");
                  }
                  

                  【讨论】:

                  • 这是一个很好的解决方案。非常快速的比较,并且只有一个长度查找!我将使用它一段时间。
                  • 很好的解决方案我在 C# 中使用了它,其中 json 中有开始和结束之类的数组:var count = list.Count; Write("[") foreach (var item in list) { Console.Write(item);写(--count > 0 ? "," : "]"); }
                  • 这是一个漂亮而优雅的解决方案,适用于除字符串类型之外的所有情况,应该被选为最佳方法。 @Matt 如果你这样做,我会支持你的问题。
                  【解决方案23】:

                  从列表中删除最后一个并保留其属性。

                  Spec spec = specs.Find(s=>s.Value == 'C');
                    if (spec != null)
                    {
                      specs.Remove(spec);
                    }
                  foreach(Spec spec in specs)
                  {
                  
                  }
                  

                  【讨论】:

                    【解决方案24】:

                    另一种有效的模式,无需重写foreach 循环:

                    var delayed = null;
                    foreach (var X in collection)
                    {
                        if (delayed != null)
                        {
                            puts("Looping");
                            // Use delayed
                        }
                        delayed = X;
                    }
                    
                    puts("Last one");
                    // Use delayed
                    

                    这样编译器保持循环正常,迭代器(包括那些没有计数的)按预期工作,最后一个与其他迭代器分开。

                    当我希望在迭代之间发生一些事情时,我也会使用这种模式,但不是在最后一次之后。那样的话X正常使用,delayed指别的东西,delay的使用只在循环开始,循环结束后什么都不用做。

                    【讨论】:

                      【解决方案25】:

                      尽可能使用join

                      大多数情况下,所有元素之间的分隔符都是相同的,只需将它们与您语言中的相应函数连接在一起即可。

                      Ruby 示例,

                      puts array.join(", ")
                      

                      这应该涵盖所有情况的 99%,如果不将数组拆分为头部和尾部。

                      Ruby 示例,

                      *head, tail = array
                      head.each { |each| put "looping: " << each }
                      puts "last element: " << tail 
                      

                      【讨论】:

                        猜你喜欢
                        • 2014-08-06
                        • 1970-01-01
                        • 2013-07-01
                        • 2015-11-12
                        • 1970-01-01
                        • 2010-09-27
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        相关资源
                        最近更新 更多