簡體   English   中英

您如何獲得 foreach 循環的當前迭代的索引?

[英]How do you get the index of the current iteration of a foreach loop?

在 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 的Select重載,您可以獲得項目( item.value )及其索引( item.i ):

函數[inside Select]的第二個參數表示源元素的索引。

new { i, value }正在創建一個新的匿名對象

如果您使用 C# 7.0 或更高版本,則可以使用ValueTuple避免堆分配:

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 當前所在的對象, MoveNextCurrent更新為下一個對象。

索引的概念與枚舉的概念是陌生的,無法做到。

因此,大多數集合都可以使用索引器和 for 循環結構進行遍歷。

與使用局部變量跟蹤索引相比,我更喜歡在這種情況下使用 for 循環。

最后,C#7 有一個不錯的語法來獲取foreach循環內的索引(即元組):

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

需要一點擴展方法:

using System.Collections.Generic;

public static class EnumExtension {
    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並使用foreach所有記錄,它會自動調用Dispose方法並關閉閱讀器(然后可以自動關閉連接)。 因此,即使您忘記關閉閱讀器,它也可以防止連接泄漏,因此更安全。

(當然,總是關閉閱讀器是一種很好的做法,但如果你不這樣做,編譯器不會捕捉到它 - 你不能保證你已經關閉了所有閱讀器,但你可以通過獲取更可能不會泄漏連接習慣於使用 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);
}

使用 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 ,我相信這是最好的解決方案。

使用計數器變量沒有任何問題。 事實上,無論您使用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()和詢問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更好,因為您仍在運行單-線程更丑陋的語法。

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

為什么:

  • foreach 看起來更好,在業務應用程序中,foreach 循環很少成為性能瓶頸
  • Foreach 在內存上的效率更高。 擁有一系列功能,而不是在每一步都轉換為新的集合。 當 CPU 緩存故障和垃圾回收次數減少時,誰會在意它是否使用更多的 CPU 周期?
  • 要求編碼者添加索引跟蹤代碼,破壞美觀
  • 它很容易實現(請微軟)並且向后兼容

雖然這里的大多數人都不是微軟員工,但這是一個正確的答案,你可以游說微軟添加這樣的功能。 您已經可以使用擴展函數構建自己的迭代器並使用元組,但微軟可以撒上語法糖來避免擴展函數

我就是這樣做的,它的簡單性/簡潔性很好,但是如果你在循環體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);
    }
}

我認為這應該不是很有效,但它有效:

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

我對這個問題的解決方案是擴展方法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 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; }
}

但是,雖然這在非 Razor 環境中會很好,如果您正在執行單個操作(即可以作為 lambda 提供的操作),但它不會成為非 Razor 上下文中 for/foreach 語法的可靠替代品.

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

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

除非您的集合可以通過某種方法返回對象的索引,否則唯一的方法是使用您的示例中的計數器。

但是,在使用索引時,唯一合理的解決方法是使用 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]});
  }
}

我認為並不總是適用,但通常足以值得一提。

無論如何,關鍵是有時在你的邏輯中已經有一個不明顯的解決方案......

這樣您就可以使用 LINQ 使用索引和值:

ListValues.Select((x, i) => new { Value = x, Index = i }).ToList().ForEach(element =>
    {
        // element.Index
        // element.Value

    });

我不確定您要根據問題對索引信息做什么。 但是,在 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)。

這樣的事情怎么樣? 請注意,如果 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;
}

這是此問題的另一種解決方案,重點是使語法盡可能接近標准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 循環,但這在此用例中不是主要問題。

這不能回答您的具體問題,但它確實為您提供了解決問題的方法:使用 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將每個輸入集合視為一個IEnumerable ,即使它是一個更具體的實現,例如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}");
});

這也避免了在其他答案中看到的任何不必要的分配。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM