在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。
此枚举器具有方法和属性:
Current返回枚举器当前所在的对象,MoveNext将Current更新到下一个对象。
索引的概念与枚举的概念不同,无法实现。
因此,大多数集合都可以使用索引器和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;
}
}
} |
使用计数器变量没有任何问题。实际上,无论您使用for、foreach、while还是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来"手动"操作IEnumerator,foreach可以节省您的特别麻烦……如果需要跳过项目,只需在循环体中使用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,您只能在List和Array上得到一个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...
} |
您可以使用它来获取每个循环的索引:
主要答案是:
显然,索引的概念与枚举的概念不同,无法实现。
虽然这在当前的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循环,但这不是这个用例中的主要问题。