使用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个回答

2

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

好东西,如果我早知道它的存在,我可能已经用了几次了。谢谢。 - Matt

2
如果您正在使用一个暴露了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);
}

如果我们还假设集合中的项目可以通过索引直接访问 - 目前接受的答案是这样假设的 - 那么普通的for循环会比foreach更加优雅/易读:

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答案可能是你能够得到的最接近的方法。
这里是Thomas答案的修复版本:
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);
}

2

你尝试做的似乎对于foreach循环来说有点过于高级了。然而,你可以明确地使用Iterator。例如,在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);
}

1

我认为我更喜欢 kgiannakakis 的解决方案,不过你也可以像这样做:

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

谢谢,这似乎是TomatoGG答案的更紧凑语法。 - Matt
2
你应该在最后一个puts之后添加“end”,而且如果你的列表是['A','B','C','A'],这个解决方案不起作用 - 它会打印两次“最后一个:A”。 - klew

1
我注意到许多建议都假定在开始循环之前可以找到列表中的最后一项,然后将每个项与此项进行比较。如果您可以有效地执行此操作,则底层数据结构可能是一个简单的数组。如果是这种情况,为什么还要使用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或类似结构,以便您可以以这种方式执行它。

听起来您正在问 “如何使用锤子将螺钉固定在最佳位置?”,当然更好的问题是,“用什么正确的工具来安装螺钉?”


1

将数组的前/后元素在执行“一般”的每个运行之前取出,这对您的情况是否是可行的解决方案?

像这样:

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

1

这个问题可以通过在函数式编程语言(如F#)中使用模式匹配来优雅地解决:

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

1

我不知道其他语言中的for-each循环是如何工作的,但在Java中。 在Java中,for-each使用了Iterable接口,该接口由for-each用于获取迭代器并循环遍历。迭代器具有一个名为hasNext的方法,您可以在循环内部看到迭代器时使用它。 实际上,您可以通过将已获得的迭代器封装在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);
  }
}

1

至少在C#中,没有普通的for循环是不可能实现这一点的。

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


0

类似于kgiannakakis的答案:

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

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