使用for each循环时如何确定最后一次循环?

50

当在对象上执行'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 循环不能仍然适用。 - Matt
1
很抱歉我只能选择一个答案,但我确实喜欢它们中的大部分。 - Matt
1
@MattChurchy,很遗憾你没有选择更好的一个。 - KingNestor
25个回答

39

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

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(",");
}

2
这是一个非常好的解决方案。非常快速的比较,只需要一次长度查找!我将在未来一段时间内使用它。 - DanielG
1
不错的解决方案,我在 C# 中使用它,就像 JSON 中的数组一样有一个开始和结束: var count = list.Count; Write("[") foreach (var item in list) { Console.Write(item); Write(--count > 0 ? "," : "]"); } - Patrik Lindström
这是一个漂亮而优雅的解决方案,适用于除字符串类型之外的所有情况,并应选择为最佳方法。@Matt 如果你这样做,我会给你的问题点赞。 - xpt

37

foreach循环(在Java中肯定是这样,在其他语言中也可能是这样)旨在表示最通用的迭代类型,包括迭代没有任何有意义的顺序集合。例如,基于哈希的集合没有排序,因此不存在“最后一个元素”。每次迭代时,最后一个迭代可能产生不同的元素。

基本上:不,foreach循环不应该以那种方式使用。


3
+1 让我困惑的是,尽管上面的 .Length/.Count 答案已经得到了很多投票,但它们为什么会得到这么多投票。 - annakata
35
在最后一个元素上做不同的事情并不意味着你希望特定的元素成为最后一个。例如,如果你有一个字符串列表需要用分隔符连接成一个大字符串。你需要循环遍历每个字符串,将它们连接起来,然后再连接你的分隔符。在最后一个字符串上,你不希望添加分隔符。因此,即使我同意为最后一个元素执行特定操作会破坏“foreach”背后的逻辑,但最后一个元素并不总是相同的事实是完全无关紧要的。 - Ksempac
一个构造物并不总是具有某个属性,这告诉我们关于它是否在我们现在感兴趣的特定情况下包含该属性的信息。如果原始帖子的意图是编写一段通用代码,可以接受任何可迭代集合并执行此操作,那么这可能是一个坏主意。但是,如果他正在谈论在他正在编写的一个特定程序中执行此操作,在那里他知道集合的其他特征,那么使用这些知识是没有问题的。 - Jay
Ruby 1.9哈希确实有顺序,它们保留插入顺序。 - David Burrows
1
这种“拒绝解决问题”的回答有一个名称吗?我不喜欢它们。David Burrows 给出了正确的答案。 - Arcolye
显示剩余2条评论

24

首先获取对最后一项引用,然后在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#不会为具有相同内容的字符串创建唯一的对象,所以不能重复使用文本字符串。
我知道基于索引的循环是要使用的。当我最初发布初始答案时,我已经说过了。在问题的背景下,我提供了我所能提供的最佳答案。我太累了,无法继续解释,所以你们可以投票删除我的答案。如果这个回答消失了,我会非常高兴,谢谢。

4
C# 3.0 将在数组上添加 .Last 运算符。 - Kirschstein
3
如果集合没有实现IList<T>接口,那么调用Last方法将涉及评估整个序列,因此性能可能较差。如果集合实现了IList<T>接口,则可以像TomatoGG上面所做的那样,只需执行类似于"var lastItem = list[list.Count - 1]"的操作即可。 - LukeH
47
这段代码容易出错。实际上您是通过值进行比较,而不是通过索引进行比较。如果存在一个与最后一个对象相等的对象,则会有几个“lastone:”行。 - bohdan_trotsenko
2
哇,这个 Stack Overflow 真的很难啊。我不知道该如何处理一个问题。是根据提问者想要的解决方案来提供解决方案,还是拒绝提供与标准和最佳实践不符的答案,成为一名标准和最佳实践的传道者呢? - anonymous
2
@TomatoGG:为了使其正常工作,您需要通过索引而不是值或引用来查找最后一个项目。如果集合无法通过索引访问,则使用“foreach”没有真正优雅的解决方案,如果可以通过索引访问集合,则普通的“for”循环将比“foreach”提供更优雅的解决方案。 - LukeH
显示剩余6条评论

22

这样是否优雅?它假设列表非空。

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

有人显然反对了,但我喜欢这个建议,它是超越传统思维的,肯定能胜任这项工作。 - Matt
4
好的,你也可以使用list[0...-1]和list[-1]代替list[0, list.length-1]和list[list.length-1],分别表示列表的第一个元素到倒数第二个元素和最后一个元素。 - Firas Assaad
1
请注意,在 Ruby 数组中,最后一个元素也可以用 list.last 表示,这可能会使最后一行代码更加清晰易懂。 - Mike Woodhouse
很好的想法。但是这种方法不能应用于哈希表,对吗? - zanbri
这是我的最爱(在精神上):它有一个很好的优点,即不会在每次迭代时执行不必要的_if检查_。 - ghilesZ

13

在 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
}

谢谢,我之前不知道这个方法,以后一定会用到的。 - Matt

9
您可以在您的类中定义一个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类。 - Matt
1
我认为这种不情愿没有理由。我猜这可能来自于方法“属于”一个类的概念,而不是通用函数。我认为如果你想要数组的新行为,你必须尽力而为。 - Svante
如果能避免块内的条件,那就更好了。如果让每个withlast都带上两个lambda,一个用于n-1项之前的项目,另一个用于第n项,我认为会更友好一些。 - molf
感谢您的回答,Svante和molf。稍微更符合惯用法的Ruby代码可能会使用each_with_index,并且Ruby通常使用snake_case来命名变量和方法。就像在这里无耻地抄袭代码一样:https://dev59.com/bnE95IYBdhLWcg3wlvCz#2241770 - Andrew Grimm

6

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);
    });
}

2
非常优雅,我喜欢!不过我更喜欢使用'while(!last)'而不是显式的break语句...并且需要初始化'last'变量。 - Thomas Levesque
这是我看到的第一个对于不是列表的可迭代对象有效的优雅解决方案。+1。 - finnw

4
如果您处理的每个项目都相同,那么应该仅使用foreach。否则,应该使用基于索引的迭代。否则,您必须添加一个不同的结构来区分foreach调用中的正常和最后一个(请参阅有关谷歌MapReduce的优秀论文,背景信息如下:http://labs.google.com/papers/mapreduce.html,其中map == foreach,reduced ==例如sum或filter)。
Map不知道结构(特别是哪个位置的项目),它只逐个转换一个项目(不能使用一个项目的任何知识来转换另一个项目!)。但是,reduce可以使用内存来计算位置并处理最后一个项目。
一个常见的技巧是反转列表并处理第一个(现在具有已知索引= 0),然后再次应用反转。(这很优雅,但不快;))

1
在许多情况下,您无法使用基于索引的迭代,因为并非所有类型的集合都支持它。例如,以 IEnumerable 接口 (.NET) 为例:您可以枚举项目,但无法通过索引访问它们... 您甚至不知道枚举中有多少项。因此,在这种情况下,foreach 是唯一的选择... - Thomas Levesque

3
你可以像这样做(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);

那个答案有什么问题吗?没有解释地投反对票真的毫无意义... - Thomas Levesque
这句话与其他答案不同,这很好。我喜欢多样化的答案。然而,我不确定这是否会打印列表中的所有项目。 - Matt
2
@MattChurchy, @TomatoGG:它将打印列表中所有非空项目,但如果列表包含任何null,则会出现错误。 - LukeH
我仍然不确定在没有编译的情况下会发生什么。但是感谢您的答复。我认为,解释结果的困难使它不太可能成为一个优雅的解决方案。 - Matt
已经测试过,能够正常工作。我终于和你在一起了。'previous'变量不是一个标志,而是保存上一个循环的结果。把最后一个变量推到循环外的列表中。- 再次感谢您的回答。 - Matt
显示剩余2条评论

3

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


我在想循环中是否可以使用'itemcount'。因为这是一个foreach循环,它没有计数,只知道当前的值,而这个值事先是不知道的。所以在if语句中我将无法进行比较。 - Matt
哦,不用在意,你确实说了“for”循环。可惜的是,在foreach循环中我无法获取列表中的当前位置。 - Matt
如果列表中的位置对您很重要,那么索引for循环优雅的解决方案,仅仅因为它很常见且经常被误用并不意味着它不优雅。 - Lazarus

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接