如何获取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个回答

25
您可以使用另一个包含索引信息的枚举器来包装原始枚举器。
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 类的代码。

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

这实际上不会返回该项的索引。相反,它将返回枚举列表中的索引,该列表可能只是列表的子列表,因此仅在子列表和列表大小相等时才提供准确数据。基本上,任何时候集合中有请求类型之外的对象,您的索引都将不正确。 - Lucas B
7
@Lucas:不会返回foreach当前迭代的索引。这就是问题所在。 - Brian Gideon

23

为什么要用foreach?!

如果你使用的是List,最简单的方法是用for代替foreach:

for (int i = 0 ; i < myList.Count ; i++)
{
    // Do something...
}

或者如果你想使用foreach:

foreach (string m in myList)
{
     // Do something...
}

你可以使用这个来知道每次循环的索引:

myList.indexOf(m)

25
使用 indexOf 方法在具有重复项的列表中是无效的,而且速度也非常慢。 - tymtam
3
需要避免的问题是多次遍历IEnumerable,例如获取项目数量然后再获取每个项目。当IEnumerable是数据库查询结果时,这会产生影响。 - David Clarke
4
myList.IndexOf() 的时间复杂度为 O(n),因此你的循环时间复杂度将会是 O(n^2)。 - Patrick Beard

21

C# 7终于给我们提供了一种优雅的方法来实现这一点:

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
是的,微软应该扩展CLR/BCL以使这种类型的事情成为本地化。 - Kind Contributor

21
以下是针对这个问题我刚想出的解决方案: 原始代码:
int index=0;
foreach (var item in enumerable)
{
    blah(item, index); // some code that depends on the index
    index++;
}

更新的代码

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

扩展方法:

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

21

这个回答建议向C#语言团队进行游说以获得直接的语言支持。

主要回答认为:

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

虽然在当前的C#语言版本(2020)中是正确的,但这并不是CLR /语言概念上的限制,它是可以做到的。

微软的C#语言开发团队可以创建一个新的C#语言功能,通过添加对新接口IIndexedEnumerable的支持来实现。

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()并且存在with var index,则编译器期望项目集合声明IIndexedEnumerable接口。如果接口不存在,则编译器可以填充源代码并将其包装为IndexedEnumerable对象,以添加跟踪索引的代码。

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

稍后,CLR可以更新以具有内部索引跟踪,仅在指定with关键字且源未直接实现IIndexedEnumerable时使用。

为什么:

  • 使用foreach循环更加美观,在业务应用程序中,foreach循环很少成为性能瓶颈
  • foreach可以更有效地利用内存。拥有函数管道而不是在每个步骤转换为新的集合。如果减少CPU缓存故障和垃圾收集,则不要在意它使用更多的CPU周期?
  • 要求编码者添加索引跟踪代码会破坏美感
  • 这很容易实现(请微软),并且向后兼容

虽然这里的大多数人不是微软员工,但这是一个正确的答案,您可以游说微软添加此功能。 您已经可以使用扩展函数和元组构建自己的迭代器,但是Microsoft可以增加语法糖来避免使用扩展函数。


等一下,这个语言特性已经存在了吗,还是提议在未来实现? - Pavel
1
@Pavel 我更新了答案以使其更清晰。这个答案是为了反驳那个声称“显然,索引的概念对枚举的概念来说是陌生的,不能实现”的领先答案而提供的。 - Kind Contributor
是的,如果JavaScript可以做到,C#应该也能做到。["John","Ben","Mike","sPoNgeBoB"].forEach((name, i)=>{ console.log(我将您名单中的第${i}个人名 ${name.toUpperCase()} 大写了。) }) - oxwilder

18

仅适用于List,而非任何IEnumerable,但在LINQ中有这个:

IList<Object> collection = new List<Object> { 
    new Object(), 
    new Object(), 
    new Object(), 
    };

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

Console.ReadLine();

@Jonathan 我并没有说这是一个好答案,我只是说它展示了可以做到他所要求的 :)

@Graphain 我不指望它会很快 - 我不太确定它是如何工作的,它可能需要每次重新迭代整个列表才能找到匹配的对象,这将进行大量比较。

话虽如此,List可能会保存每个对象的索引以及计数。

如果Jonathan能详细说明一下他的更好的想法就好了。

然而,在foreach中只需保持一个计数器来跟踪进度会更好,更简单,也更具适应性。


7
不确定为什么会被狂踩。虽然性能限制很大,但你确实回答了问题! - Matt Mitchell
5
这个方法存在的问题是,它只在列表中的项目是唯一的情况下才有效。 - CodesInChaos

11
这是我做的方法,因为它简单/简洁,但如果你在循环体中做了很多事情,obj.Value会变得相当老套。
foreach(var obj in collection.Select((item, index) => new { Index = index, Value = item }) {
    string foo = string.Format("Something[{0}] = {1}", obj.Index, obj.Value);
    ...
}

9
// using foreach loop how to get index number:
    
foreach (var result in results.Select((value, index) => new { index, value }))
{
    // do something
}

6
虽然这段代码可能回答了问题,但提供关于它是如何解决问题以及为什么这样做的额外说明会提高答案的长期价值。 - Klaus Gütter
3
这只是这个已有答案的重复。 - Pang

5
您可以像这样编写您的循环:
var s = "ABCDEFG";
foreach (var item in s.GetEnumeratorWithIndex())
{
    System.Console.WriteLine("Character: {0}, Position: {1}", item.Value, item.Index);
}

在添加以下结构体和扩展方法之后:
该结构体和扩展方法封装了Enumerable.Select功能。
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);
    }
}

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

这适用于支持 IList 的集合。

73
两个问题:1)因为在大多数实现中,IndexOf 的时间复杂度为 O(n),所以这是 O(n^2) 的。2)如果列表中有重复的项,则会失败。 - CodesInChaos
17
注意:O(n^2) 表示对于大型集合来说可能速度非常慢。 - O'Rooney
使用IndexOf方法真的太棒了!这正是我在foreach循环中获取索引(编号)所寻找的!非常感谢。 - Mitja Bonca
21
天啊,我希望你没用那个!:( 它确实使用了你不想创建的那个变量——事实上,它会创建n+1个整数,因为该函数也必须创建一个才能返回——而且索引搜索比每步操作中的一个整数递增要慢得多。为什么人们不会将此答案投下去呢? - canahari
14
请勿使用此答案,我在其中一条评论中发现了所提到的残酷真相:"如果列表中存在重复项,则此方法会失败。"!!! - Bruce
在调试会话中非常有帮助。 - Amit

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