关于C#:如何获取foreach循环当前迭代的索引?

How do you get the index of the current iteration of a foreach loop?

在C中是否有一些我从未遇到过的罕见的语言构造(比如我最近学到的一些,一些堆栈溢出)来获取表示foreach循环当前迭代的值?

例如,我现在根据情况做类似的事情:

1
2
3
4
5
6
int i = 0;
foreach (Object o in collection)
{
    // ...
    i++;
}


伊恩·默瑟在菲尔·哈克的博客上发布了类似的解决方案:

1
2
3
4
5
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

the second parameter of the function [inside Select] represents the index of the source element.

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

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

1
2
3
4
5
foreach (var item in Model.Select((value, i) => ( value, i )))
{
    var value = item.value;
    var index = item.i;
}

您还可以通过使用自动销毁来消除item.

1
2
3
4
5
6
foreach ((MyType value, Int32 i) in Model.Select((value, i) => ( value, i )))
{
    <li id="item_@i">@value
</li>

}


foreach用于迭代实现IEnumerable的集合。它通过调用集合上的GetEnumerator来实现这一点,该集合将返回Enumerator

此枚举器具有方法和属性:

  • MOVENTXEL()
  • 电流

Current返回枚举器当前所在的对象,MoveNextCurrent更新到下一个对象。

索引的概念与枚举的概念不同,无法实现。

因此,大多数集合都可以使用索引器和for循环构造进行遍历。

与使用局部变量跟踪索引相比,我更喜欢在这种情况下使用for循环。


可以这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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));
    }
}


我不同意这样的观点:在大多数情况下,for循环是更好的选择。

foreach是一个有用的构造,在任何情况下都不能由for循环替换。

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

(当然,总是关闭读卡器是一种很好的做法,但如果不这样做,编译器就不会捕获它了-您不能保证已经关闭了所有的读卡器,但您可以通过养成使用foreach的习惯使其更可能不会泄漏连接。)

还有其他一些例子表明,Dispose方法的隐式调用是有用的。


最后,C 7有一个很好的语法来获取foreach循环(即元组)中的索引:

1
2
3
4
foreach (var (item, index) in collection.WithIndex())
{
    Debug.WriteLine($"{index}: {item}");
}

需要一个小的扩展方法:

1
2
public static IEnumerable<(T item, int index)> WithIndex<T>(this IEnumerable<T> self)      
   => self.Select((item, index) => (item, index));


字面上的回答——警告,性能可能不如使用int跟踪索引好。至少比使用IndexOf要好。

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

1
2
3
4
5
6
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);
}


使用@flyswat的答案,我提出了这个解决方案:

1
2
3
4
5
6
7
8
9
//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循环进行循环。然而,技巧是使循环的条件为cx1(2)。

由于枚举器的MoveNext方法如果有下一个元素并且可以访问它,则返回true,这使得循环条件使循环在我们用完要迭代的元素时停止。


使用linq、c_7和System.ValueTuplenuget包,可以执行以下操作:

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

您可以使用常规的foreach构造,并且能够直接访问值和索引,而不是作为对象的成员,并且只将这两个字段保留在循环的范围内。基于这些原因,我认为如果您能够使用C 7和System.ValueTuple,这是最好的解决方案。


可以用另一个包含索引信息的枚举器包装原始枚举器。

1
2
3
4
5
6
7
foreach (var item in ForEachHelper.WithIndex(collection))
{
    Console.Write("Index=" + item.Index);
    Console.Write(";Value=" + item.Value);
    Console.Write(";IsLast=" + item.IsLast);
    Console.WriteLine();
}

这是ForEachHelper类的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public static class ForEachHelper
{
    public sealed class Item<T>
    {
        public int Index { get; set; }
        public T Value { get; set; }
        public bool IsLast { get; set; }
    }

    public static IEnumerable<Item<T>> WithIndex<T>(IEnumerable<T> enumerable)
    {
        Item<T> item = null;
        foreach (T value in enumerable)
        {
            Item<T> next = new Item<T>();
            next.Index = 0;
            next.Value = value;
            next.IsLast = false;
            if (item != null)
            {
                next.Index = item.Index + 1;
                yield return item;
            }
            item = next;
        }
        if (item != null)
        {
            item.IsLast = true;
            yield return item;
        }            
    }
}


使用计数器变量没有任何问题。实际上,无论您使用forforeachwhile还是do,都必须在某处声明并递增一个计数器变量。

因此,如果您不确定是否有适当的索引集合,请使用此成语:

1
2
3
4
5
var i = 0;
foreach (var e in collection) {
   // Do stuff with 'e' and 'i'
   i++;
}

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

1
2
3
4
5
// 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来"手动"操作IEnumeratorforeach可以节省您的特别麻烦……如果需要跳过项目,只需在循环体中使用continue

为了完整起见,根据您对索引所做的操作(上面的构造提供了很大的灵活性),您可以使用Parallel Linq:

1
2
3
4
5
6
7
8
9
10
11
12
// 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,您只能在ListArray上得到一个ForEach()扩展方法。现在还不清楚使用它是否比使用简单的foreach更好,因为您仍然在运行单线程以获得更糟糕的语法。


这里有一个解决这个问题的方法

原代码:

1
2
3
4
5
6
int index=0;
foreach (var item in enumerable)
{
    blah(item, index); // some code that depends on the index
    index++;
}

更新代码

1
enumerable.ForEach((item, index) => blah(item, index));

扩展方法:

1
2
3
4
5
6
7
8
9
10
11
    public static IEnumerable<T> ForEach<T>(this IEnumerable<T> enumerable, Action<T, int> action)
    {
        var unit = new Unit(); // unit is a new type from the reactive framework (http://msdn.microsoft.com/en-us/devlabs/ee794896.aspx) to represent a void, since in C# you can't return a void
        enumerable.Select((item, i) =>
            {
                action(item, i);
                return unit;
            }).ToList();

        return pSource;
    }

1
2
3
4
5
int index;
foreach (Object o in collection)
{
    index = collection.indexOf(o);
}

这对支持IList的托收有效。


它只适用于列表,不适用于任何IEnumerable,但在Linq中有:

1
2
3
4
5
6
7
8
9
10
11
12
IList<Object> collection = new List<Object> {
    new Object(),
    new Object(),
    new Object(),
    };

foreach (Object o in collection)
{
    Console.WriteLine(collection.IndexOf(o));
}

Console.ReadLine();

@乔纳森,我没有说这是一个很好的答案,我只是说,它只是表明有可能做他要求的事情:)

@我不希望它是快速的-我不完全确定它是如何工作的,它可以通过整个列表重复每次找到一个匹配的对象,这将是一个地狱般的比较。

也就是说,list可能会保留每个对象的索引和计数。

乔纳森似乎有个更好的主意,如果他愿意详细说明的话?

不过,最好记下你的前臂在哪里,更简单,适应性更强。


C 7最终为我们提供了一种优雅的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
static class Extensions
{
    public static IEnumerable<(int, T)> Enumerate<T>(
        this IEnumerable<T> input,
        int start = 0
    )
    {
        int i = start;
        foreach (var t in input)
        {
            yield return (i++, t);
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        var s = new string[]
        {
           "Alpha",
           "Bravo",
           "Charlie",
           "Delta"
        };

        foreach (var (i, t) in s.Enumerate())
        {
            Console.WriteLine($"{i}: {t}");
        }
    }
}


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

1
2
3
4
5
6
int i = 0;
foreach (var item in Collection)
{
    item.index = i;
    ++i;
}

我就是这样做的,它的简单性和简洁性很好,但是如果你在循环体obj.Value中做了很多,它会很快变老。

1
2
3
4
foreach(var obj in collection.Select((item, index) => new { Index = index, Value = item }) {
    string foo = string.Format("Something[{0}] = {1}", obj.Index, obj.Value);
    ...
}

为什么要说?!

如果使用列表,最简单的方法是使用for而不是foreach。

1
2
3
4
for(int i = 0 ; i < myList.Count ; i++)
{
    // Do Something...
}

或者如果要使用foreach:

1
2
3
4
foreach (string m in myList)
{
     // Do something...      
}

您可以使用它来获取每个循环的索引:

1
myList.indexOf(m)


主要答案是:

显然,索引的概念与枚举的概念不同,无法实现。

虽然这在当前的C版本中是正确的,但这不是概念上的限制。

MS创建一个新的C语言特性可以解决这个问题,同时支持一个新的接口IIndexenumerable。

1
2
3
4
5
6
7
8
9
10
foreach (var item in collection with var index)
{
    Console.WriteLine("Iteration {0} has value {1}", index, item);
}

//or, building on @user1414213562's answer
foreach (var (item, index) in collection)
{
    Console.WriteLine("Iteration {0} has value {1}", index, item);
}

如果foreach传递了一个IEnumerable,并且无法解析IIndexedEnumerable,但使用var index请求它,则C编译器可以用一个indexedEnumerable对象包装源,该对象添加了用于跟踪索引的代码。

1
2
3
4
5
interface IIndexedEnumerable<T> : IEnumerable<T>
{
    //Not index, because sometimes source IEnumerables are transient
    public long IterationNumber { get; }
}

为什么?

  • foreach看起来更好,在业务应用程序中很少会成为性能瓶颈
  • foreach可以提高内存效率。在每个步骤中都有一个函数管道,而不是转换为新的集合。谁在乎它是否使用更多的CPU周期,是否有更少的CPU缓存错误和更少的GC.Collection
  • 要求编码人员添加索引跟踪代码,破坏美观
  • 它很容易实现(感谢MS),并且向后兼容

虽然这里的大多数人不是微软,但这是一个正确的答案,您可以游说微软添加这样的功能。您已经可以使用扩展函数构建自己的迭代器并使用元组,但是MS可以撒上语法甜头以避免扩展函数


最好使用关键字continue这样的安全构造

1
2
3
4
5
6
7
8
int i=-1;
foreach (Object o in collection)
{
    ++i;
    //...
    continue; //<--- safe to call, index will be increased
    //...
}


如果集合是列表,则可以使用list.indexof,如:

1
2
3
4
5
foreach (Object o in collection)
{
    // ...
    @collection.IndexOf(o)
}


我对这个问题的解决方案是扩展方法WithIndex()

http://code.google.com/p/ub-dotnet-utilities/source/browse/trunk/src/utilities/extensions/enumerableextensions.cs

像它一样使用它

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

var odd = list.WithIndex().Where(i => (i.Item & 1) == 1);
CollectionAssert.AreEqual(new[] { 0, 2, 4 }, odd.Select(i => i.Index));
CollectionAssert.AreEqual(new[] { 1, 3, 5 }, odd.Select(i => i.Item));


出于兴趣,Phil Haack在Razor模板化的代表(http://haacked.com/archive/2011/04/14/a-better-razor-foreach-loop.aspx)的背景下写了一个这样的例子。

实际上,他编写了一个扩展方法,该方法将迭代包装在"iterateditem"类中(见下文),允许在迭代期间访问索引和元素。

1
2
3
4
5
6
7
8
9
public class IndexedItem<TModel> {
  public IndexedItem(int index, TModel item) {
    Index = index;
    Item = item;
  }

  public int Index { get; private set; }
  public TModel Item { get; private set; }
}

但是,如果您在非Razor环境中执行单个操作(即可以作为lambda提供的操作),那么在非Razor环境中,这将不会是for/foreach语法的可靠替代。


我不认为这会很有效率,但它是有效的:

1
2
3
@foreach (var banner in Model.MainBanners) {
    @Model.MainBanners.IndexOf(banner)
}


我在林肯帕德建造了这个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var listOfNames = new List<string>(){"John","Steve","Anna","Chris"};

var listCount = listOfNames.Count;

var NamesWithCommas = string.Empty;

foreach (var element in listOfNames)
{
    NamesWithCommas += element;
    if(listOfNames.IndexOf(element) != listCount -1)
    {
        NamesWithCommas +=",";
    }
}

NamesWithCommas.Dump();  //LINQPad method to write to console.

您也可以使用string.join

1
var joinResult = string.Join(",", listOfNames);


您可以这样编写循环:

1
2
3
4
5
var s ="ABCDEFG";
foreach (var item in s.GetEnumeratorWithIndex())
{
    System.Console.WriteLine("Character: {0}, Position: {1}", item.Value, item.Index);
}

在添加以下结构和扩展方法之后。

结构和扩展方法封装了可枚举的。请选择功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public struct ValueWithIndex<T>
{
    public readonly T Value;
    public readonly int Index;

    public ValueWithIndex(T value, int index)
    {
        this.Value = value;
        this.Index = index;
    }

    public static ValueWithIndex<T> Create(T value, int index)
    {
        return new ValueWithIndex<T>(value, index);
    }
}

public static class ExtensionMethods
{
    public static IEnumerable<ValueWithIndex<T>> GetEnumeratorWithIndex<T>(this IEnumerable<T> enumerable)
    {
        return enumerable.Select(ValueWithIndex<T>.Create);
    }
}

像这样的怎么样?注意,如果myEnumeratable为空,myDelimitedString可能为空。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
IEnumerator enumerator = myEnumerable.GetEnumerator();
string myDelimitedString;
string current = null;

if( enumerator.MoveNext() )
    current = (string)enumerator.Current;

while( null != current)
{
    current = (string)enumerator.Current; }

    myDelimitedString += current;

    if( enumerator.MoveNext() )
        myDelimitedString += DELIMITER;
    else
        break;
}


我刚刚遇到了这个问题,但是在我的案例中,思考这个问题给出了最佳的解决方案,与预期的解决方案无关。

这可能是一个很常见的情况,基本上,我正在从一个源列表中读取数据,并在目标列表中基于它们创建对象,但是,我必须首先检查源项是否有效,并希望返回任何错误的行。乍一看,我想将索引放入当前属性的对象枚举器中,但是,在复制这些元素时,我仍然从当前目标隐式地知道当前索引。显然,它取决于目标对象,但对我来说,它是一个列表,而且很可能实现ICollection。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var destinationList = new List<someObject>();
foreach (var item in itemList)
{
  var stringArray = item.Split(new char[] { ';', ',' }, StringSplitOptions.RemoveEmptyEntries);

  if (stringArray.Length != 2)
  {
    //use the destinationList Count property to give us the index into the stringArray list
    throw new Exception("Item at row" + (destinationList.Count + 1) +" has a problem.");
  }
  else
  {
    destinationList.Add(new someObject() { Prop1 = stringArray[0], Prop2 = stringArray[1]});
  }
}

我认为,不一定总是适用的,但常常足够值得一提。

不管怎样,关键是有时在你的逻辑中已经有了一个不明显的解决方案…


除非集合可以通过某种方法返回对象的索引,否则唯一的方法是使用类似于示例中的计数器。

但是,在处理索引时,唯一合理的解决方法是使用for循环。其他任何东西都会带来代码复杂性,更不用说时间和空间复杂性了。


我不相信有一种方法可以获得foreach循环当前迭代的值。数数自己,似乎是最好的方法。

我可以问一下,你为什么想知道?

看来你最有可能做的是以下三件事之一:

1)从集合中获取对象,但在本例中,您已经拥有了它。

2)对对象进行计数以备日后处理…集合具有可供使用的Count属性。

3)根据对象在循环中的顺序设置该对象的属性……尽管在将该对象添加到集合中时可以轻松设置该属性。


我不知道你是怎么处理基于这个问题的索引信息的。但是,在C中,您通常可以调整IEnumerable.Select方法以从所需的内容中获取索引。例如,对于一个值是奇数还是偶数,我可能使用类似的方法。

1
2
3
4
string[] names = {"one","two","three" };
var oddOrEvenByName = names
    .Select((name, index) => new KeyValuePair<string, int>(name, index % 2))
    .ToDictionary(kvp => kvp.Key, kvp => kvp.Value);

这会给你一本字典,名字是奇数(1)还是偶数(0)。


这并不能回答您的特定问题,但它确实为您的问题提供了解决方案:使用for循环在对象集合中运行。然后您将得到正在处理的当前索引。

1
2
3
4
5
// Untested
for (int i = 0; i < collection.Count; i++)
{
    Console.WriteLine("My index is" + i);
}

我想从理论上更深入地讨论这个问题(因为它已经有了足够的实际答案)

.NET有一个非常好的数据组抽象模型(也称为集合)

  • 在最顶层,最抽象的是,您有一个IEnumerable,它只是一组您可以枚举的数据。不管你如何枚举,你只需要枚举一些数据就行了。这个枚举是由一个完全不同的对象完成的,一个IEnumerator

这些接口定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//
// Summary:
//     Exposes an enumerator, which supports a simple iteration over a non-generic collection.
public interface IEnumerable
{
    //
    // Summary:
    //     Returns an enumerator that iterates through a collection.
    //
    // Returns:
    //     An System.Collections.IEnumerator object that can be used to iterate through
    //     the collection.
    IEnumerator GetEnumerator();
}

//
// Summary:
//     Supports a simple iteration over a non-generic collection.
public interface IEnumerator
{
    //
    // Summary:
    //     Gets the element in the collection at the current position of the enumerator.
    //
    // Returns:
    //     The element in the collection at the current position of the enumerator.
    object Current { get; }

    //
    // Summary:
    //     Advances the enumerator to the next element of the collection.
    //
    // Returns:
    //     true if the enumerator was successfully advanced to the next element; false if
    //     the enumerator has passed the end of the collection.
    //
    // Exceptions:
    //   T:System.InvalidOperationException:
    //     The collection was modified after the enumerator was created.
    bool MoveNext();
    //
    // Summary:
    //     Sets the enumerator to its initial position, which is before the first element
    //     in the collection.
    //
    // Exceptions:
    //   T:System.InvalidOperationException:
    //     The collection was modified after the enumerator was created.
    void Reset();
}
  • 正如您可能注意到的,IEnumerator接口不"知道"索引是什么,它只知道它当前指向的元素以及如何移动到下一个元素。

  • 现在的诀窍是:foreach把每个输入集合都看作一个IEnumerable,即使它是一个更具体的实现,比如IList(它继承自IEnumerable),它只会看到抽象的接口IEnumerable

  • 实际上,foreach正在做的是,在集合上调用GetEnumerator,并调用MoveNext,直到返回false。

  • 所以这里有一个问题,你想在一个抽象概念"可枚举"上定义一个具体的概念"索引",内置的foreach构造并没有给你这个选项,所以你唯一的方法就是自己定义它,或者通过你最初做的(手工创建计数器)或者仅仅使用IEnumerator的一个实现来识别对索引进行索引并实现一个识别该自定义实现的foreach构造。

我个人会创建这样的扩展方法

1
2
3
4
5
6
7
8
9
10
11
12
public static class Ext
{
    public static void FE<T>(this IEnumerable<T> l, Action<int, T> act)
    {
        int counter = 0;
        foreach (var item in l)
        {
            act(counter, item);
            counter++;
        }
    }
}

像这样使用

1
2
3
4
5
var x = new List<string>() {"hello","world" };
x.FE((ind, ele) =>
{
    Console.WriteLine($"{ind}: {ele}");
});

这也避免了在其他答案中看到任何不必要的分配。


这里是这个问题的另一个解决方案,重点是尽可能使语法接近标准的foreach

如果你想让你的视图在MVC中看起来漂亮整洁,这种构造是很有用的。例如,不要用通常的方式(很难很好地格式化)编写:

1
2
3
4
5
6
7
 <%int i=0;
 foreach (var review in Model.ReviewsList) { %>
   ">
        <%:review.Title%>                      
   
    <%i++;
 } %>

你可以这样写:

1
2
3
4
5
 <%foreach (var review in Model.ReviewsList.WithIndex()) { %>
   ">
        <%:review.Title%>                      
   
 <%} %>

我已经编写了一些辅助方法来启用此功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public static class LoopHelper {
    public static int Index() {
        return (int)HttpContext.Current.Items["LoopHelper_Index"];
    }      
}

public static class LoopHelperExtensions {
    public static IEnumerable<T> WithIndex<T>(this IEnumerable<T> that) {
        return new EnumerableWithIndex<T>(that);
    }

    public class EnumerableWithIndex<T> : IEnumerable<T> {
        public IEnumerable<T> Enumerable;

        public EnumerableWithIndex(IEnumerable<T> enumerable) {
            Enumerable = enumerable;
        }

        public IEnumerator<T> GetEnumerator() {
            for (int i = 0; i < Enumerable.Count(); i++) {
                HttpContext.Current.Items["LoopHelper_Index"] = i;
                yield return Enumerable.ElementAt(i);
            }
        }

        IEnumerator IEnumerable.GetEnumerator() {
            return GetEnumerator();
        }
    }

在非Web环境中,您可以使用static而不是HttpContext.Current.Items

这本质上是一个全局变量,因此不能嵌套多个WITHINDEX循环,但这不是这个用例中的主要问题。