How do you get the index of the current iteration

2018-12-31 12:45发布

Is there some rare language construct I haven't encountered (like the few I've learned recently, some on Stack Overflow) in C# to get a value representing the current iteration of a foreach loop?

For instance, I currently do something like this depending on the circumstances:

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

标签: c# foreach
30条回答
美炸的是我
2楼-- · 2018-12-31 13:23

The leading answer states:

"Obviously, the concept of an index is foreign to the concept of enumeration, and cannot be done."

While this is true of the current C# version, this is not a conceptual limit.

The creation of a new C# language feature by MS could solve this, along with support for a new Interface 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);
}

If foreach is passed an IEnumerable and can't resolve an IIndexedEnumerable, but it is asked with var index, then the C# compiler can wrap the source with an IndexedEnumerable object, which adds in the code for tracking the index.

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

Why:

  • Foreach looks nicer, and in business applications is rarely a performance bottleneck
  • Foreach can be more efficient on memory. Having a pipeline of functions instead of converting to new collections at each step. Who cares if it uses a few more CPU cycles, if there are less CPU cache faults and less GC.Collects
  • Requiring the coder to add index tracking code, spoils the beauty
  • It's quite easy to implement (thanks MS) and is backward compatible

While most people here are not MS, this is a correct answer, and you can lobby MS to add such a feature. You could already build your own iterator with an extension function and use tuples, but MS could sprinkle the syntactic sugar to avoid the extension function

查看更多
像晚风撩人
3楼-- · 2018-12-31 13:24

Using LINQ, C# 7, and the System.ValueTuple NuGet package, you can do this:

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

You can use the regular foreach construct and be able to access the value and index directly, not as a member of an object, and keeps both fields only in the scope of the loop. For these reasons, I believe this is the best solution if you are able to use C# 7 and System.ValueTuple.

查看更多
余生请多指教
4楼-- · 2018-12-31 13:24

Here's a solution I just came up with for this problem

Original code:

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

Updated code

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

Extension Method:

    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;
    }
查看更多
余生无你
5楼-- · 2018-12-31 13:25

C# 7 finally gives us an elegant way to do this:

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}");
        }
    }
}
查看更多
时光乱了年华
6楼-- · 2018-12-31 13:26

You can write your loop like this:

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

After adding the following struct and extension method.

The struct and extension method encapsulate Enumerable.Select functionality.

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);
    }
}
查看更多
时光乱了年华
7楼-- · 2018-12-31 13:27

Unless your collection can return the index of the object via some method, the only way is to use a counter like in your example.

However, when working with indexes, the only reasonable answer to the problem is to use a for loop. Anything else introduces code complexity, not to mention time and space complexity.

查看更多
登录 后发表回答