在 C# 中是否有一些我没有遇到过的罕见语言构造(比如我最近学到的一些,一些在 Stack Overflow 上)来获取表示 foreach 循环当前迭代的值?

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

int i = 0;
foreach (Object o in collection)
{
    // ...
    i++;
}
有帮助吗?

解决方案

foreach 用于迭代实现的集合 IEnumerable. 。它通过调用来做到这一点 GetEnumerator 在集合上,这将返回一个 Enumerator.

该枚举器有一个方法和一个属性:

  • 移动下一个()
  • 当前的

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

索引的概念与枚举的概念是陌生的,无法完成。

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

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

其他提示

Ian Mercer 发布了与此类似的解决方案 菲尔·哈克的博客:

foreach (var item in Model.Select((value, i) => new { i, value }))
{
    var value = item.value;
    var index = item.i;
}

这会给你带来物品(item.value) 及其索引 (item.i) 通过使用 Linq 的这种超载 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. 通过使用自动解构:

<ol>
foreach ((MyType value, Int32 i) in Model.Select((value, i) => ( value, i )))
{
    <li id="item_@i">@value</li>
}
</ol>

可以做这样的事情:

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

最后,C#7 有一个不错的语法,可以在 a 中获取索引 foreach 卢普(i.e.元组):

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

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

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

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

(当然,始终关闭读取器是一种很好的做法,但如果您不这样做,编译器将不会捕获它 - 您不能保证已关闭所有读取器,但您可以通过获取来使您更有可能不会泄漏连接习惯使用foreach。)

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

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

使用@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,这使得当我们用完要迭代的元素时,循环条件使循环停止。

使用 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.

您可以用另一个包含索引信息的枚举器包装原始枚举器。

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

使用计数器变量没有任何问题。事实上,无论你使用 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 在循环体中。

只是为了完整性,这取决于你是什么 正在做 对于您的索引(上述构造提供了足够的灵活性),您可以使用并行 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, ,因为您仍在运行单线程以获得更丑陋的语法。

这是我刚刚针对这个问题想出的解决方案

原始代码:

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.

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

它只适用于 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();

@乔纳森我并没有说这是一个很好的答案,我只是说这只是表明可以按照他的要求去做:)

@Graphain我不希望它很快 - 我不完全确定它是如何工作的,它可以每次重复整个列表以找到匹配的对象,这将是大量的比较。

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

乔纳森似乎有一个更好的主意,他能否详细说明一下?

不过,最好只在 foreach 中记录您正在做的事情,这样更简单,适应性也更强。

只需添加您自己的索引即可。把事情简单化。

int i = 0;
foreach (var item in Collection)
{
    item.index = i;
    ++i;
}

这就是我的做法,这很简单/简洁,但是如果您在循环体中做了很多事情 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);
    ...
}

为什么要foreach?!

最简单的方法是使用 为了 而不是 foreach 如果您正在使用列表 .

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

或者如果你想使用 foreach :

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

您可以使用它来了解每个 Loop 的索引:

myList.indexOf(m)

最好使用关键字 continue 安全施工是这样的

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

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

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

主要答案指出:

“显然,索引的概念与枚举的概念是陌生的,并且无法做到。”

虽然当前的 C# 版本确实如此,但这并不是概念上的限制。

MS 创建的新 C# 语言功能以及对新接口 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 index 进行询问,则 C# 编译器可以使用 IndexedEnumerable 对象包装源,该对象会添加用于跟踪索引的代码。

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

为什么:

  • Foreach 看起来更好,并且在业务应用程序中很少成为性能瓶颈
  • Foreach 可以更有效地利用内存。拥有一个函数管道,而不是在每一步都转换为新的集合。如果 CPU 缓存故障较少且 GC.Collects 较少,谁会在乎它是否会多使用几个 CPU 周期呢?
  • 要求编码器添加索引跟踪代码,破坏了美观
  • 它很容易实现(感谢 MS)并且向后兼容

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

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

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

使用它就像

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”类中(见下文),允许在迭代期间访问索引和元素。

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 语法的可靠替代品。

我认为这应该不是很有效,但它确实有效:

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

你可以这样写你的循环:

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

添加以下结构和扩展方法后。

结构体和扩展方法封装了 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);
    }
}

我不相信有办法获取 foreach 循环当前迭代的值。数数自己,似乎是最好的办法。

请问,你为什么想知道?

看来您很可能会做以下三件事之一:

1)从集合中获取对象,但在本例中您已经拥有它。

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

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

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

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

像这样的事情怎么样?请注意,如果 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;
}

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

这可能是一种很常见的情况,基本上,我正在从一个源列表中读取内容并在目标列表中基于它们创建对象,但是,我必须首先检查源项目是否有效并希望返回任何行错误。乍一看,我想将索引获取到 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)。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top