在 C# 中是否有一些我没有遇到过的罕见语言结构(比如我最近学到的一些,一些在 Stack Overflow 上)来获取代表 foreach 循环当前迭代的值?
例如,我目前根据情况做这样的事情:
int i = 0;
foreach (Object o in collection)
{
// ...
i++;
}
Ian Mercer 在Phil Haack 的博客上发布了与此类似的解决方案:
foreach (var item in Model.Select((value, i) => new { i, value }))
{
var value = item.value;
var index = item.i;
}
通过使用LINQ 的重载,您可以获得项目 ( item.value
) 及其索引 ( ) :item.i
Select
函数[inside Select]的第二个参数表示源元素的索引。
new { i, value }
正在创建一个新的匿名对象。
ValueTuple
如果您使用的是 C# 7.0 或更高版本,则可以通过使用来避免堆分配:
foreach (var item in Model.Select((value, i) => ( value, i )))
{
var value = item.value;
var index = item.i;
}
您还可以item.
通过使用自动解构来消除:
foreach (var (value, i) in Model.Select((value, i) => ( value, i )))
{
// Access `value` and `i` directly here.
}
foreach
用于迭代IEnumerable
实现. 它通过调用GetEnumerator
集合来执行此操作,该集合将返回一个Enumerator
.
这个 Enumerator 有一个方法和一个属性:
MoveNext()
Current
Current
返回 Enumerator 当前所在的对象,MoveNext
更新Current
到下一个对象。
索引的概念与枚举的概念是陌生的,无法做到。
因此,大多数集合都可以使用索引器和 for 循环结构进行遍历。
与使用局部变量跟踪索引相比,我更喜欢在这种情况下使用 for 循环。
最后,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));
可以做这样的事情:
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并使用 a 遍历所有记录,foreach
它会自动调用Dispose方法并关闭阅读器(然后可以自动关闭连接)。因此,即使您忘记关闭阅读器,它也可以防止连接泄漏,因此更安全。
(当然,总是关闭阅读器是一种很好的做法,但如果你不这样做,编译器不会捕捉到它 - 你不能保证你已经关闭了所有阅读器,但你可以通过获取更可能不会泄漏连接习惯于使用 foreach。)
可能还有其他Dispose
有用的方法的隐式调用示例。
字面答案——警告,性能可能不如仅使用 anint
来跟踪索引。至少它比使用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);
}
使用 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
.
使用计数器变量没有任何问题。事实上,无论您使用或for
,计数器变量都必须在某处声明和递增。foreach
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'
}
永远不需要IEnumerator
通过调用MoveNext()
和询问来“手动”操作Current
-foreach
为您节省了特别的麻烦......如果您需要跳过项目,只需continue
在循环体中使用 a 。
为了完整起见,根据您对索引所做的操作(上述结构提供了很大的灵活性),您可以使用 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,您只能在and ... 上获得一个ForEach()
扩展方法,List<T>
Array
并且不清楚使用它是否比做一个简单的 更好foreach
,因为您仍在运行单线程以获得更丑陋的语法。
使用@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,因此当我们用完要迭代的元素时,循环条件会使循环停止。
您可以用另一个包含索引信息的枚举器包装原始枚举器。
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;
}
}
}
只需添加您自己的索引。把事情简单化。
int i = 0;
foreach (var item in Collection)
{
item.index = i;
++i;
}
这是我刚刚为这个问题提出的解决方案
原始代码:
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;
}
为什么要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)
它只适用于 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 可能会保留每个对象的索引以及计数。
乔纳森似乎有一个更好的主意,如果他能详细说明的话?
最好只计算你在 foreach 中的位置,更简单,更适应。
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}");
}
}
}
这个答案:游说 C# 语言团队以获得直接的语言支持。
领先的答案指出:
显然,索引的概念与枚举的概念是陌生的,无法做到。
虽然当前 C# 语言版本 (2020) 确实如此,但这不是概念上的 CLR/语言限制,它可以做到。
Microsoft C# 语言开发团队可以通过添加对新接口 IIndexedEnumerable 的支持来创建新的 C# 语言功能
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 对象对源代码进行 polyfill 包装,该对象会添加用于跟踪索引的代码。
interface IIndexedEnumerable<T> : IEnumerable<T>
{
//Not index, because sometimes source IEnumerables are transient
public long IterationNumber { get; }
}
稍后,CLR 可以更新为具有内部索引跟踪,仅在with
指定关键字且源不直接实现时使用IIndexedEnumerable
为什么:
虽然这里的大多数人都不是微软员工,但这是一个正确的答案,你可以游说微软添加这样的功能。您已经可以使用扩展函数构建自己的迭代器并使用元组,但微软可以撒上语法糖来避免扩展函数
我就是这样做的,它的简单性/简洁性很好,但如果你在循环体中做了很多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);
...
}
int index;
foreach (Object o in collection)
{
index = collection.indexOf(o);
}
这适用于支持IList
.
//using foreach loop how to get index number:
foreach (var result in results.Select((value, index) => new { index, value }))
{
//do something
}
最好使用continue
这样的关键字安全构造
int i=-1;
foreach (Object o in collection)
{
++i;
//...
continue; //<--- safe to call, index will be increased
//...
}
你可以这样写你的循环:
var s = "ABCDEFG";
foreach (var item in s.GetEnumeratorWithIndex())
{
System.Console.WriteLine("Character: {0}, Position: {1}", item.Value, item.Index);
}
添加以下结构和扩展方法后。
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);
}
}
我对这个问题的解决方案是扩展方法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));
出于兴趣,Phil Haack 刚刚在 Razor Templated Delegate 的上下文中写了一个示例(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 语法的可靠替代品.
我认为这应该不是很有效,但它有效:
@foreach (var banner in Model.MainBanners) {
@Model.MainBanners.IndexOf(banner)
}
我在LINQPad中构建了这个:
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);
除非您的集合可以通过某种方法返回对象的索引,否则唯一的方法是使用示例中的计数器。
但是,在使用索引时,唯一合理的解决方法是使用 for 循环。其他任何东西都会引入代码复杂性,更不用说时间和空间复杂性了。
我不相信有一种方法可以获取 foreach 循环当前迭代的值。数数自己,似乎是最好的办法。
请问,你为什么想知道?
似乎您最可能会做以下三件事之一:
1) 从集合中获取对象,但在这种情况下,您已经拥有它。
2) 为以后的后期处理计算对象...集合具有可以使用的 Count 属性。
3) 根据对象在循环中的顺序设置对象的属性...尽管您可以在将对象添加到集合时轻松设置该属性。
我刚遇到这个问题,但在我的案例中考虑这个问题给出了最好的解决方案,与预期的解决方案无关。
这可能是很常见的情况,基本上,我正在从一个源列表中读取并在目标列表中基于它们创建对象,但是,我必须首先检查源项目是否有效并想要返回任何错误。乍一看,我想在 Current 属性中将索引放入对象的枚举器中,但是,当我复制这些元素时,我隐含地知道当前目标的当前索引。显然它取决于你的目标对象,但对我来说它是一个列表,而且很可能它会实现 ICollection。
IE
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]});
}
}
我认为并不总是适用,但通常足以值得一提。
无论如何,关键是有时在你的逻辑中已经有一个不明显的解决方案......
我不确定您要根据问题对索引信息做什么。但是,在 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)。
如果集合是列表,则可以使用 List.IndexOf,如下所示:
foreach (Object o in collection)
{
// ...
@collection.IndexOf(o)
}
这样的事情怎么样?请注意,如果 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;
}
这样您就可以使用 LINQ 使用索引和值:
ListValues.Select((x, i) => new { Value = x, Index = i }).ToList().ForEach(element =>
{
// element.Index
// element.Value
});
这是此问题的另一种解决方案,重点是使语法尽可能接近标准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 环境中,您可以使用 astatic
而不是HttpContext.Current.Items
.
这本质上是一个全局变量,因此不能嵌套多个 WithIndex 循环,但这在此用例中不是主要问题。
这不能回答您的具体问题,但它确实为您提供了解决问题的方法:使用 for 循环遍历对象集合。那么您将拥有正在处理的当前索引。
// Untested
for (int i = 0; i < collection.Count; i++)
{
Console.WriteLine("My index is " + i);
}
我想从理论上讨论这个问题(因为它已经有足够的实际答案)
.net 对数据组(又名集合)有一个非常好的抽象模型
IEnumerable
它只是一组你可以枚举的数据。你如何枚举并不重要,只是你可以枚举一些数据。这个枚举是由一个完全不同的对象完成的,一个IEnumerator
这些接口定义如下:
//
// 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
考虑每个输入集合 an IEnumerable
,即使它是更具体的实现,例如 an IList<T>
(继承自IEnumerable
),它也只会看到抽象接口IEnumerable
。
实际在做什么foreach
,是调用GetEnumerator
集合,并调用MoveNext
直到它返回 false。
所以这就是问题所在,您想在抽象概念“Enumerables”上定义一个具体概念“Indices”,内置foreach
构造没有给你这个选项,所以你唯一的方法是你自己定义它,或者你最初是在做(手动创建一个计数器),或者只是使用一个IEnumerator
识别索引的实现并实现一个foreach
识别该自定义实现的构造。
我个人会创建一个这样的扩展方法
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++;
}
}
}
并像这样使用它
var x = new List<string>() { "hello", "world" };
x.FE((ind, ele) =>
{
Console.WriteLine($"{ind}: {ele}");
});
这也避免了在其他答案中看到的任何不必要的分配。