提问



是否有一些罕见的语言构造我没有遇到过(就像我最近学到的一些,有些是关于Stack Overflow)在C#中得到一个表示foreach循环当前迭代的值?


例如,我目前根据具体情况做这样的事情:


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

最佳参考


foreach用于迭代实现IEnumerable的集合。它通过在集合上调用GetEnumerator来执行此操作,该集合将返回Enumerator[93] [94] [95]


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



  • 的MoveNext()

  • 电流



Current返回Enumerator当前所在的对象,MoveNextCurrent更新为下一个对象。


显然,索引的概念对于枚举的概念来说是陌生的,并且无法完成。


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


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

其它参考1


Ian Mercer在Phil Haack的博客上发布了类似的解决方案:[96]


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):[97]



  函数[[select Select]]的第二个参数表示源元素的索引。



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

其它参考2


可以做这样的事情:


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

其它参考3


我不同意在大多数情况下for循环是更好的选择的评论。


foreach是一个有用的结构,在所有情况下都不能被for循环替换。


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


(当然总是关闭读者是好的做法,但是如果你不这样做,编译器就不会抓住它 - 你不能保证你已经关闭了所有的读者,但是你可以更有可能通过获取来避免泄漏连接习惯使用foreach。)


可能存在Dispose方法的隐式调用有用的其他示例。

其它参考4


字面答案 - 警告,性能可能不如仅使用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);
}

其它参考5


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


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


需要一点扩展方法:


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

其它参考6


使用@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,因此当我们用尽元素进行迭代时,使循环条件使循环停止。

其它参考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类的代码。


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

其它参考8


这是我刚刚提出的解决这个问题的解决方案


原始代码:


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

其它参考9


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


如果你不确定你是否有一个适当索引的集合,请使用这个习惯用法:


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


如果你知道你的可索引集合是O(1)用于索引访问(它将用于Array并且可能用于List<T>,否则使用这个]](文档没有说),但不一定适用于其他类型(如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()来手动操作IEnumerator并且询问Current - 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,你只能获得ForEach()扩展在List<T>Array上的方法......并且不清楚使用它比做一个简单的foreach更好,因为你仍然在运行单线程以获得更粗糙的语法。

其它参考10


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


这适用于支持IList的集合。

其它参考11


使用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,这是最好的解决方案。

其它参考12


它只适用于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我不希望它快速 - 我不完全确定它是如何工作的,它可以在每次重复整个列表中找到一个匹配的对象,这将是一个比较的helluvalot。


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


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


最好只计算你在foreach中所处的位置,更简单,更具适应性。

其它参考13


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

其它参考14


这就是我这样做的原因,这很简单/简洁,但是如果你在循环体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);
    ...
}

其它参考15


为什么要foreach?!


最简单的方法是使用代替,而不是foreach ,如果您使用的是List


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


或者如果你想使用foreach:


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


你可以用它来表示每个循环的khow索引:


myList.indexOf(m)

其它参考16


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


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

其它参考17


我在LINQPad中构建了这个:[100]


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:


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

其它参考18


如果集合是列表,则可以使用List.IndexOf,如下所示:


foreach (Object o in collection)
{
    // ...
    @collection.IndexOf(o)
}

其它参考19


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


请问,为什么你想知道?


看起来你最喜欢做三件事之一:


1)从集合中获取对象,但在这种情况下,您已经拥有它。


2)计算对象以供以后后处理...集合具有您可以使用的Count属性。


3)根据循环中的顺序在对象上设置属性...虽然您可以在将对象添加到集合时轻松设置该属性。

其它参考20


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


http://code.google.com/p/ub-dotnet-utilities/source/browse/trunk/Src/Utilities/Extensions/EnumerableExtensions.cs[101]


一样使用它


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

其它参考21


这样的事怎么样?请注意,如果myEnumerable为空,myDelimitedString可能为null。


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

其它参考22


为了兴趣,Phil Haack在Razor Templated Delegate(http://haacked.com/archive/2011/04/14/a-better-razor-foreach-loop.aspx)的背景下写了一个这样的例子[102]


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


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提供的操作),这将是正常的,但它不会成为非for/foreach语法的可靠替代剃刀背景。

其它参考23


我不确定你要根据问题对索引信息做什么。但是,在C#中,你通常可以调整IEnumerable.Select方法来获取你想要的索引。例如,我可能会使用这样的事情是值是奇数还是偶数。


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)的字典。

其它参考24


我不认为这应该是非常有效的,但它有效:


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

其它参考25


你可以像这样写你的循环:


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


添加以下struct和extension方法之后。


struct和extension方法封装了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);
    }
}

其它参考26


主要答案是:


显然,指数的概念对于枚举的概念来说是陌生的,而且无法完成。


虽然目前的C#版本也是如此,但这不是概念上的限制。


MS创建新的C#语言功能可以解决这个问题,同时支持新的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);
}


如果foreach传递了一个IEnumerable并且无法解析一个IIndexedEnumerable,但是它被赋予var索引,那么C#编译器可以使用一个IndexedEnumerable对象包装它,该对象会添加用于跟踪索引的代码。


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


为什么:



  • Foreach看起来更好,在商业应用程序中很少是性能瓶颈

  • Foreach可以更有效地记忆。拥有一系列功能,而不是在每一步都转换为新的集合。谁关心它是否使用了更多的CPU周期,如果CPU缓存故障较少且GC较少。收集

  • 要求编码人员添加索引跟踪代码,破坏美丽

  • 它很容易实现(感谢MS)并且向后兼容



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

其它参考27


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


但是,在使用索引时,问题的唯一合理答案是使用for循环。其他任何东西都会引入代码复杂性,更不用说时间和空间的复杂性。

其它参考28


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


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





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


我认为并不总是适用,但往往足以值得一提。


无论如何,关键是有时候你的逻辑中已经存在一个非显而易见的解决方案......

其它参考29


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


如果您希望在MVC中使您的视图看起来漂亮和干净,那么这种构造非常有用。例如,而不是以通常的方式编写这个(很难很好地格式化):


 <%int i=0;
 foreach (var review in Model.ReviewsList) { %>
    <div id="review_<%=i%>">
        <h3><%:review.Title%></h3>                      
    </div>
    <%i++;
 } %>


你可以写这个:


 <%foreach (var review in Model.ReviewsList.WithIndex()) { %>
    <div id="review_<%=LoopHelper.Index()%>">
        <h3><%:review.Title%></h3>                      
    </div>
 <%} %>


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


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循环,但这不是此用例中的主要问题。