你如何获得foreach循环的当前迭代的索引?
是否有一些罕见的语言构造我没有遇到(像我最近学到的一些堆栈溢出一些)在C#中获得一个值代表一个foreach循环的当前迭代?
例如,我现在根据情况做这样的事情:
int i=0; foreach (Object o in collection) { // ... i++; }
foreach
是迭代实现IEnumerable
集合。 它通过调用集合上的GetEnumerator
来完成,这将返回一个Enumerator
。
这个枚举器有一个方法和一个属性:
- 的MoveNext()
- 当前
Current
返回Enumerator Current
的对象, MoveNext
Current
更新为下一个对象。
显然,索引的概念对于枚举的概念是陌生的,不能做到。
因此,大多数集合都可以使用索引器和for循环构造来遍历。
我非常喜欢在这种情况下使用for循环,而不是使用局部variables来跟踪索引。
Ian Mercer在Phil Haack的博客上发布了类似的解决scheme:
foreach (var item in Model.Select((value, i) => new { i, value })) { var value = item.value; var index = item.i; }
这通过使用Linq的Select
重载获取item( item.value
)及其索引( item.i
):
函数[select]中的第二个参数表示源元素的索引。
new { i, value }
正在创build一个新的匿名对象 。
可以做这样的事情:
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
循环是更好的select。
foreach
是一个有用的构造,在任何情况下都不能被for
循环replace。
例如,如果您有一个DataReader并使用foreach
循环遍历所有logging,它将自动调用Dispose方法并closures阅读器(然后可以自动closures连接)。 因此,即使您忘记closures阅读器,也可以防止连接泄漏,因此更安全。
(确保总是closures读者是一个很好的习惯,但是如果你不这样做的话,编译器就不会去捕捉它 – 你不能保证你已经closures了所有的读者,但是你可以使它更有可能不会通过获取在使用foreach的习惯。)
Dispose
方法的隐式调用可能还有其他的例子是有用的。
文字答案 – 警告,性能可能不如使用int
来跟踪索引。 至less比使用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}", oi, ox); }
用@ FlySwat的答案,我想出了这个解决scheme:
//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,所以当我们用完元素迭代时,循环条件会使循环停止。
您可以将原始枚举器与包含索引信息的另一个枚举器进行封装。
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; } } }
最后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));
我刚刚提出了一个解决这个问题的解决scheme
原始码:
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; }
int index; foreach (Object o in collection) { index = collection.indexOf(o); }
这将支持IList
collections。
使用计数器variables没有任何问题。 实际上,无论你使用foreach
, foreach
或foreach
while
do
必须声明和增加一个计数器variables。
所以,如果你不确定你是否有适当的索引集合,使用这个习惯用法:
var i = 0; foreach (var e in collection) { // Do stuff with 'e' and 'i' i++; }
否则使用这一个,如果你知道你的索引集合是O(1)的索引访问(这将是Array
和大概为List<T>
(文档没有说),但不一定是其他types作为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
更好,因为你还在运行single-为了更丑陋的语法。
这就是我如何做到的,它的简单性和简洁性很好,但是如果你在循环体obj.Value
做了很多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); ... }
最好使用关键字continue
安全的build设
int i=-1; foreach (Object o in collection) { ++i; //... continue; //<--- safe to call, index will be increased //... }
为什么要这样?
如果您使用List ,最简单的方法是使用而不是foreach。
for(int i = 0 ; i < myList.Count ; i++) { // Do Something... }
或者如果你想使用foreach:
foreach (string m in myList) { // Do something... }
你可以用这个来显示每个Loop的索引:
myList.indexOf(m)
如果集合是一个列表,则可以使用List.IndexOf,如下所示:
foreach (Object o in collection) { // ... @collection.IndexOf(o) }
它只能用于列表而不是任何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我不期望它快 – 我不完全确定它是如何工作的,它可以重复通过整个列表每次find一个匹配的对象,这将是一个helluvalot比较。
也就是说,List可能会保留每个对象的索引以及计数。
乔纳森似乎有一个更好的主意,如果他会详细说明?
尽pipe更简单,适应性更强一些,但是要注意哪些地方可以做到这一点。
我在LINQPad中build立了这个:
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);
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}"); } } }
我不相信有一种方法来获得foreach循环的当前迭代的值。 数着自己,似乎是最好的方法。
请问,为什么你想知道?
你似乎最愿意做三件事之一:
1)从集合中获取对象,但在这种情况下,你已经拥有了它。
2)计算对象以供日后后处理…集合具有您可以使用的Count属性。
3)根据循环中的顺序在对象上设置一个属性…虽然在将对象添加到集合时可以很容易地设置它。
我对这个问题的解决scheme是一个WithIndex()
的扩展方法,
像使用它
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));
这样的事情呢? 请注意,如果myEnumerable为空,myDelimitedString可能为空。
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; }
感兴趣的是,Phil Haack刚刚在一个Razor模板化代表的文章中写了一个这样的例子( http://haacked.com/archive/2011/04/14/a-better-razor-foreach-loop.aspx )
实际上,他写了一个扩展方法,将迭代封装在一个“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; } }
然而,如果你正在做一个单一的操作(也就是可以作为一个lambda提供的操作),那么在非Razor环境中这样做可能会很好,但是它不会成为非Razor上下文中for / foreach语法的稳定替代。
基于这个问题,我不确定你想要怎么处理索引信息。 但是,在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)在列表中。
我不认为这应该是相当有效的,但它的工作原理:
@foreach (var banner in Model.MainBanners) { @Model.MainBanners.IndexOf(banner) }
你可以这样写你的循环:
var s = "ABCDEFG"; foreach (var item in s.GetEnumeratorWithIndex()) { System.Console.WriteLine("Character: {0}, Position: {1}", item.Value, item.Index); }
添加以下结构和扩展方法后。
结构和扩展方法封装了Enumerable.Selectfunction。
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); } }
主要答案是:
“显然,一个指数的概念对于枚举概念来说是陌生的,不可能做到。”
虽然目前的C#版本是这样,但这不是一个概念上的限制。
MS创build一个新的C#语言function可以解决这个问题,同时支持一个新的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并且不能parsing一个IIndexedEnumerable,但是它被询问了var index,那么C#编译器就可以用一个IndexedEnumerable对象来包装源代码,这会在代码中追加索引。
interface IIndexedEnumerable<T> : IEnumerable<T> { //Not index, because sometimes source IEnumerables are transient public long IterationNumber { get; } }
为什么:
- Foreach看起来更好,在商业应用程序中很less是性能瓶颈
- Foreach可以更有效地记忆。 拥有一系列function,而不是在每一步转换为新的集合。 谁在乎它是否使用了更多的CPU周期,如果有更less的CPUcaching故障和更less的GC.Collect
- 要求编码员添加索引跟踪代码,破坏美丽
- 这很容易实现(感谢MS),并向后兼容
虽然这里的大多数人不是MS,这是一个正确的答案,你可以游说MS添加这样的function。 你已经可以用扩展函数构build你自己的迭代器, 并且使用元组 ,但是MS可以使用语法糖来避免扩展函数
除非你的集合可以通过某种方法返回对象的索引,唯一的办法就是像你的例子那样使用一个计数器。
但是,使用索引时,唯一合理的答案是使用for循环。 除此之外,还有其他一些代码复杂性,更不用说时间和空间的复杂性
我刚刚遇到了这个问题,但在我的案例中思考问题给出了最好的解决scheme,与预期的解决scheme无关。
这可能是一个很常见的情况,基本上,我正在从一个源列表中读取,并在目标列表中创build基于它们的对象,但是,我必须先检查源项是否有效,并且要返回任何错误。 乍看之下,我想要索引到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]}); } }
我认为,并不总是适用,但往往足以值得一提。
无论如何,问题的关键是,有时你已经有了一个非显而易见的解决scheme。
这是另一个解决这个问题的方法,重点是保持语法尽可能接近标准的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(); } }
In a non-web environment you could use a static
instead of HttpContext.Current.Items
.
This is essentially a global variable, and so you cannot have more than one WithIndex loop nested, but that is not a major problem in this use case.
This doesn't answer your specific question, but it DOES provide you with a solution to your problem: use a for loop to run through the object collection. then you will have the current index you are working on.
// Untested for (int i = 0; i < collection.Count; i++) { Console.WriteLine("My index is " + i); }