如何获取foreach循环的当前迭代的索引?

1291

有没有一些我还没有遇到的罕见语言结构(就像我最近在 Stack Overflow 上学到的一些)可以在 C# 中获得表示当前 foreach 循环迭代的值?

例如,根据情况,我目前会做类似于以下的操作:

int i = 0;
foreach (Object o in collection)
{
    // ...
    i++;
}

4
使用foreach进行转换检索通常不会比在集合上使用基于索引的访问更优化,尽管在许多情况下它们将是相等的。 foreach的目的是使您的代码可读性更好,但它通常会添加一个间接层,这不是免费的。 - Brian
17
我认为 foreach 的主要目的是为所有集合提供一个通用的迭代机制,无论它们是否可索引(如 List)或不可索引(如 Dictionary)。 - Brian Gideon
2
嗨,Brian Gideon - 我完全同意(这是几年前,当时我经验不足)。然而,虽然 Dictionary 不能被索引,但 Dictionary 的迭代会按特定顺序遍历它(即一个枚举器是可索引的,因为它按顺序产生元素)。在这个意义上,我们可以说我们不是在寻找集合内的索引,而是当前枚举元素在枚举中的索引(即我们是否处于第一个、第五个或最后一个枚举元素)。 - Matt Mitchell
9
foreach 还允许编译器在编译后的代码中跳过每个数组访问的边界检查。使用带索引的 for 循环将使运行时检查您的索引访问是否安全。 - IvoTops
1
但这是错误的。如果您在循环内部不更改 for 循环的迭代变量,则编译器知道其边界,并且不需要再次检查它们。这是非常普遍的情况,任何体面的编译器都会实现它。 - Jim Balter
显示剩余3条评论
38个回答

963

Ian Mercer在Phil Haack的博客上发布了与此类似的解决方案:

foreach (var item in Model.Select((value, i) => new { i, value }))
{
    var value = item.value;
    var index = item.i;
}

使用 LINQ 的 Select该重载函数 可以获取项(item.value)及其索引(item.i):

函数内部的第二个参数表示源元素的索引。

new { i, value } 创建了一个新的匿名对象

如果您使用的是 C# 7.0 或更高版本,可以使用 ValueTuple 来避免堆分配:

foreach (var item in Model.Select((value, i) => ( value, i )))
{
    var value = item.value;
    var index = item.i;
}

你也可以通过使用自动解构来消除 item.:

foreach (var (value, i) in Model.Select((value, i) => ( value, i )))
{
    // Access `value` and `i` directly here.
}

15
这个解决方案适用于 Razor 模板的情况下,如果模板的整洁度是设计考虑中的一个关键点,并且您想要使用枚举到的每个项的索引。但请注意,“包装”导致的对象分配会增加额外的开销(空间和时间),这是在对整数进行必要递增的基础上的。 - David Bullock
6
@mjsr 这里是文档的链接(https://msdn.microsoft.com/en-us/library/bb534869(v=vs.110).aspx)。 - Thorkil Holm-Jacobsen
61
抱歉 - 这很聪明,但它真的比在 foreach 循环外创建索引并在每次循环中递增更易读吗? - jbyrd
23
使用较新版本的C#,你也可以使用元组,代码如下: foreach (var (item, i) in Model.Select((v, i) => (v, i))) 这样可以使用元组解构在for循环中直接访问项(item)和索引(i)。请注意,不要改变原意,并确保翻译通俗易懂。 - Haukman
24
有人能解释一下为什么这是一个好的回答吗?(在撰写本文时有超过450个赞)就我所看到的,它比简单地递增计数器更难理解,因此不易于维护,占用更多内存,并且可能更慢。我错过了什么吗? - Rich N
显示剩余6条评论

664
foreach用于迭代实现IEnumerable的集合。它通过在集合上调用GetEnumerator来实现,该方法将返回一个Enumerator
此枚举器具有一种方法和一种属性:
  • MoveNext()
  • Current
Current返回枚举器当前正在处理的对象,MoveNextCurrent更新为下一个对象。
索引的概念与枚举的概念不同,因此无法使用索引进行迭代。
因此,大多数集合都可以使用索引器和for循环结构来遍历。
在这种情况下,我非常喜欢使用for循环,而不是使用本地变量跟踪索引。

229
“显然,索引的概念与枚举的概念不同,因此无法完成。”--这是无意义的,正如David B和bcahill的回答所表明的那样。索引是在范围内进行枚举,没有理由不能并行枚举两个事物...这正是Enumerable.Select的索引形式所做的。 - Jim Balter
18
基本代码示例:for(var i = 0; i < myList.Count; i ++){System.Diagnostics.Debug.WriteLine(i);} - Chad Hedgcock
7
@Pretzel 我引用的那个声明(显然)是不正确的,我已经解释了原因。链表“没有索引”这一事实完全无关紧要,并显示出极度混乱。 - Jim Balter
4
“底线是,‘链表是一个没有索引的IEnumerable的例子’这个说法是一个草人论,与实际无关。没有人声称所有的IEnumerable都‘有索引’。” - Jim Balter
9
@Pretzel Jim的观点是只要能够将元素映射到一系列整数,就可以对其进行索引。该类本身不存储索引并不重要。此外,链表确实具有顺序,这只会加强Jim的立场。你所需做的就是按顺序为每个元素编号。具体来说,您可以在迭代时通过增加计数器来实现此操作,或者您可以生成与相同长度的整数列表,然后使用它们进行配对(如Python的zip函数)。 - jpmc26
显示剩余7条评论

493
终于,C# 7.0为在foreach循环中获取索引(即元组)提供了一个不错的语法。
foreach (var (item, index) in collection.WithIndex())
{
    Debug.WriteLine($"{index}: {item}");
}

需要一个小的扩展方法:
using System.Collections.Generic;

public static class IEnumerableExtensions {
    public static IEnumerable<(T item, int index)> WithIndex<T>(this IEnumerable<T> self)       
       => self.Select((item, index) => (item, index));
}

31
这个答案被低估了,拥有元组会更加简洁。 - Kind Contributor
35
修改以处理空集合:public static IEnumerable<(T item, int index)> WithIndex<T>(this IEnumerable<T> self) => self?.Select((item, index) => (item, index)) ?? new List<(T, int)>(); 修改后的代码可以处理传入null的集合,如果传入的集合为null,则返回一个空列表。原本的意思没有改变,只是添加了对空集合的处理。 - 2Toad
6
将方法称为 Enumerated 可能更容易被习惯于其他语言的人识别(也许还可以交换元组参数的顺序)。不过 WithIndex 本身已经很明显了。 - FernAndr
11
针对空条件,您也可以使用 Enumerable.Empty<(T, int)>(),比创建一个空列表更加高效。 - Dan Diplo
好的回答,只是请不要将其命名为“EnumExtensions”,那样会很令人困惑。它与枚举没有任何关系。只需将其命名为IEnumerableExtensions即可。 - undefined

128

可以这样做:

public static class ForEachExtensions
{
    public static void ForEachWithIndex<T>(this IEnumerable<T> enumerable, Action<T, int> handler)
    {
        int idx = 0;
        foreach (T item in enumerable)
            handler(item, idx++);
    }
}

public class Example
{
    public static void Main()
    {
        string[] values = new[] { "foo", "bar", "baz" };

        values.ForEachWithIndex((item, idx) => Console.WriteLine("{0}: {1}", idx, item));
    }
}

14
这并没有“真正”解决问题。这个想法很好,但它并没有避免使用额外的计数变量。 - Atmocreations
如果我们在for循环内部有一个return语句,那么这种方法就行不通了。如果你把"ForEachWithIndex"改成那个,那么它就不是通用的了,最好还是写一个普通的for循环。 - Shankar Raju
1
你的 ForEachWithIndex 调用相当于使用 Linq Select 方法,该方法接受一个字符串和索引: values.Select((item, idx) => { Console.WriteLine("{0}: {1}", idx, item); return item; }).ToList(); - user2023861

114

我不同意那些认为在大多数情况下使用 for 循环更好的评论。

foreach 是一个有用的结构,在某些情况下无法被 for 循环替代。

例如,如果您有一个 DataReader 并使用 foreach 循环遍历所有记录,它会自动调用 Dispose 方法并关闭读取器(这样可以自动关闭连接)。因此,即使您忘记关闭阅读器,也更安全,因为它可以防止连接泄漏。

(当然,总是关闭读取器是一个良好的实践方法,但编译器不会捕捉到它,如果您不关闭读取器,您不能保证已经关闭了所有读取器,但您可以通过习惯使用 foreach 来使泄漏连接的可能性更小。)

还可能有其他隐式调用 Dispose 方法的例子是有用的。


2
感谢您指出这一点。相当微妙。您可以在http://www.pvle.be/2010/05/foreach-statement-calls-dispose-on-ienumerator/和http://msdn.microsoft.com/en-us/library/aa664754(VS.71).aspx获取更多信息。 - Mark Meuer
+1. 我在 Programmers.SE 上更详细地写了关于 foreach 如何与 for 不同(并更接近 while)的内容。 - Arseni Mourzenko
1
虽然关于foreach和for的差异的帖子很有趣,但这根本没有回答问题,问题是“如何获取foreach循环的当前迭代索引”。 - TylerH

71

字面回答 -- 警告:性能可能不如仅使用int跟踪索引。但至少比使用IndexOf好。

你只需要使用Select的索引重载,将集合中的每个项都包装在一个匿名对象中,该对象知道索引。这可以针对实现IEnumerable的任何内容进行操作。

System.Collections.IEnumerable collection = Enumerable.Range(100, 10);

foreach (var o in collection.OfType<object>().Select((x, i) => new {x, i}))
{
    Console.WriteLine("{0} {1}", o.i, o.x);
}

3
使用OfType<T>()而不使用Cast<T>()的唯一理由是,如果枚举中的某些项可能无法进行显式转换,则需要使用OfType<T>()。对于对象来说,永远不会出现这种情况。 - dahlbyk
15
当然,使用 OfType 而非 Cast 的另一个原因是我从未使用过 Cast。 - Amy B
1
为什么需要使用OfType()(或Cast,如果更喜欢)?Select可以直接在集合上调用,不是吗? - StayOnTarget
1
@UuDdLrLrSs 不行。Select 需要一个类型参数。非泛型的 IEnumerable 没有可供提供的类型参数。 - Amy B
1
@AmyB 谢谢!我忽视了集合只是IEnumerable。 - StayOnTarget

57

使用 LINQ、C# 7 和 System.ValueTuple NuGet 包,您可以这样做:

foreach (var (value, index) in collection.Select((v, i)=>(v, i))) {
    Console.WriteLine(value + " is at index " + index);
}

您可以使用普通的foreach语句,并且能够直接访问值和索引,而不是作为对象成员,并且在循环的范围内保留这两个字段。因此,如果您能够使用C# 7和System.ValueTuple,我认为这是最好的解决方法。


3
这是不同的,因为 .Select 是 LINQ 中内置的。你不需要编写自己的功能?不过,你需要安装 VS 的 "System.ValueTuple"。 - Anton
运行得非常好,而且非常简单。谢谢。 - Kathara

47
Great! How can I assist you? What text do you need to be translated and into what language?
var i = 0;
foreach (var e in collection) {
   // Do stuff with 'e' and 'i'
   i++;
}

如果您知道您的可索引集合在索引访问方面是O(1),则使用此方法(对于Array和可能对于List<T>(文档没有说明),它将是O(1),但对于其他类型(如LinkedList)则不一定):

// Hope the JIT compiler optimises read of the 'Count' property!
for (var i = 0; i < collection.Count; i++) {
   var e = collection[i];
   // Do stuff with 'e' and 'i'
}

不应该通过调用MoveNext()和查询Current来“手动”操作IEnumerator - foreach正为您省去了这个麻烦...如果需要跳过项目,只需在循环体中使用continue。另外,根据您对索引的使用方式(上述结构提供了丰富的灵活性),您可能会使用Parallel LINQ。
// First, filter 'e' based on 'i',
// then apply an action to remaining 'e'
collection
    .AsParallel()
    .Where((e,i) => /* filter with e,i */)
    .ForAll(e => { /* use e, but don't modify it */ });

// Using 'e' and 'i', produce a new collection,
// where each element incorporates 'i'
collection
    .AsParallel()
    .Select((e, i) => new MyWrapper(e, i));

我们在上面使用了AsParallel(),因为现在已经是2014年了,我们希望充分利用那些多核来加快速度。此外,在“顺序”LINQ中,你只能在List<T>Array上获取ForEach()扩展方法...而且并不清楚使用它是否比使用简单的foreach更好,因为你仍然在单线程下运行,语法更丑陋。

1
在我看来,这是最好的答案,因为它考虑了可读性和确保有边界安全。 - Dean P

39

借鉴了@FlySwat的回答,我想出了这个解决方案:

//var list = new List<int> { 1, 2, 3, 4, 5, 6 }; // Your sample collection

var listEnumerator = list.GetEnumerator(); // Get enumerator

for (var i = 0; listEnumerator.MoveNext() == true; i++)
{
  int currentItem = listEnumerator.Current; // Get current item.
  //Console.WriteLine("At index {0}, item is {1}", i, currentItem); // Do as you wish with i and  currentItem
}

您可以使用GetEnumerator获取枚举器,然后使用for循环进行迭代。但是,关键在于使循环条件为listEnumerator.MoveNext() == true

由于枚举器的MoveNext方法会返回true,如果有下一个元素可以访问,则将其作为循环条件,这样当我们遍历完所有元素时,循环就会停止。


14
不需要比较listEnumerator.MoveNext() == true。这就像询问计算机true == true? :) 只需说if listEnumerator.MoveNext() { } - Zesty
11
@Zesty,你说得完全正确。我觉得在这种情况下将其添加进去更易读,特别是对于不习惯除 i < blahSize 以外的条件的人来说。 - Gezim
2
你应该释放枚举器。 - Antonín Lejsek
1
@EdwardBrey 您说得对,这是一个很好的观点。但是在这里谈论 listEnumerator,它是一个通用枚举器,因此它确实实现了 IDisposable 并应该被处理。 - Antonín Lejsek
1
@AntonínLejsek 对于 List<T> 的枚举器,你发现得很好。它实现了 System.Collections.Generic.IEnumerator<T>,该接口继承自 IDisposableList<T> 的枚举器在 Dispose 中不执行任何操作,也没有终结器,因此在这种情况下调用 Dispose 没有任何效果,但对于其他可枚举对象可能会有影响。 - Edward Brey
显示剩余2条评论

34

只需要添加自己的索引即可。保持简单。

int i = -1;
foreach (var item in Collection)
{
    ++i;
    item.index = i;
}

1
最好从-1开始i,并在进入循环时递增它。 - Howard
1
为什么它更好? - conterio
int i = 0; foreach (var item in Collection) { if (item.value == 3) continue; ++i; } - Howard
好的,我会更新我的回答。 - conterio
似乎不是一个好主意,因为它容易出现+1错误。 - Dmitry Kh
@DmitryKh,你能详细说明一下吗? - conterio

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