Wie erhält man den Index der aktuellen Iteration einer foreach-Schleife?

StackOverflow https://stackoverflow.com/questions/43021

  •  09-06-2019
  •  | 
  •  

Frage

Gibt es in C# ein seltenes Sprachkonstrukt, auf das ich noch nicht gestoßen bin (wie die wenigen, die ich kürzlich gelernt habe, einige auf Stack Overflow), um einen Wert zu erhalten, der die aktuelle Iteration einer foreach-Schleife darstellt?

Je nach den Umständen mache ich derzeit zum Beispiel so etwas:

int i = 0;
foreach (Object o in collection)
{
    // ...
    i++;
}
War es hilfreich?

Lösung

Der foreach dient zum Durchlaufen von Sammlungen, die implementiert werden IEnumerable.Dies geschieht durch einen Aufruf GetEnumerator auf der Sammlung, die eine zurückgibt Enumerator.

Dieser Enumerator hat eine Methode und eine Eigenschaft:

  • MoveNext()
  • Aktuell

Current gibt das Objekt zurück, auf dem sich Enumerator gerade befindet, MoveNext Aktualisierung Current zum nächsten Objekt.

Das Konzept eines Index ist dem Konzept der Aufzählung fremd und kann nicht umgesetzt werden.

Aus diesem Grund können die meisten Sammlungen mithilfe eines Indexers und des for-Schleifenkonstrukts durchlaufen werden.

In dieser Situation verwende ich lieber eine for-Schleife als den Index mit einer lokalen Variablen zu verfolgen.

Andere Tipps

Ian Mercer hat eine ähnliche Lösung wie diese gepostet Phil Haacks Blog:

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

Dadurch erhalten Sie den Artikel (item.value) und sein Index (item.i) durch die Nutzung diese Überladung von Linqs Select:

Der zweite Parameter der Funktion [inside Select] stellt den Index des Quellelements dar.

Der new { i, value } schafft ein neues anonymes Objekt.

Heap-Zuweisungen können durch die Verwendung vermieden werden ValueTuple Wenn Sie C# 7.0 oder höher verwenden:

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

Sie können das auch beseitigen item. durch automatische Destrukturierung:

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

Könnte so etwas machen:

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

Schließlich verfügt C#7 über eine anständige Syntax zum Abrufen eines Index innerhalb von a foreach Schleife (d. h.e.Tupel):

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

Eine kleine Erweiterungsmethode wäre erforderlich:

public static IEnumerable<(T item, int index)> WithIndex<T>(this IEnumerable<T> self)       
   => self.Select((item, index) => (item, index)); 

Ich stimme den Kommentaren nicht zu, dass a for Schleife ist in den meisten Fällen die bessere Wahl.

foreach ist ein nützliches Konstrukt und nicht durch a ersetzbar for Schleife unter allen Umständen.

Wenn Sie beispielsweise eine haben DataReader und durchlaufen Sie alle Datensätze mit a foreach es ruft automatisch das auf Entsorgen -Methode und schließt den Reader (der dann die Verbindung automatisch schließen kann).Dies ist daher sicherer, da es Verbindungslecks verhindert, selbst wenn Sie vergessen, das Lesegerät zu schließen.

(Natürlich ist es eine gute Praxis, Leser immer zu schließen, aber der Compiler wird es nicht abfangen, wenn Sie dies nicht tun. Sie können nicht garantieren, dass Sie alle Leser geschlossen haben, aber Sie können die Wahrscheinlichkeit erhöhen, dass keine Verbindungen verloren gehen, indem Sie erhalten in der Gewohnheit, foreach zu verwenden.)

Möglicherweise gibt es noch andere Beispiele für den impliziten Aufruf von Dispose Methode nützlich ist.

Wörtliche Antwort – Warnung, die Leistung ist möglicherweise nicht so gut wie die einfache Verwendung von int um den Index zu verfolgen.Zumindest ist es besser als die Verwendung IndexOf.

Sie müssen lediglich die Indexüberladung von Select verwenden, um jedes Element in der Sammlung mit einem anonymen Objekt zu umschließen, das den Index kennt.Dies kann gegen alles durchgeführt werden, das IEnumerable implementiert.

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

Mithilfe der Antwort von @FlySwat habe ich folgende Lösung gefunden:

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

Sie erhalten den Enumerator mit GetEnumerator und dann schleifen Sie mit a for Schleife.Der Trick besteht jedoch darin, die Bedingung der Schleife festzulegen listEnumerator.MoveNext() == true.

Seit der MoveNext Die Methode eines Enumerators gibt „true“ zurück, wenn es ein nächstes Element gibt und auf dieses zugegriffen werden kann. Dadurch sorgt die Schleifenbedingung dafür, dass die Schleife stoppt, wenn uns die Elemente ausgehen, über die wir iterieren können.

Mit LINQ, C# 7 und dem System.ValueTuple Mit dem NuGet-Paket können Sie Folgendes tun:

foreach (var (value, index) in collection.Select((v, i)=>(v, i))) {
    Console.WriteLine(value + " is at index " + index);
}

Sie können das reguläre verwenden foreach erstellen und direkt auf den Wert und den Index zugreifen können, nicht als Mitglied eines Objekts, und behält beide Felder nur im Gültigkeitsbereich der Schleife.Aus diesen Gründen glaube ich, dass dies die beste Lösung ist, wenn Sie C# 7 verwenden können und System.ValueTuple.

Sie könnten den ursprünglichen Enumerator mit einem anderen umschließen, der die Indexinformationen enthält.

foreach (var item in ForEachHelper.WithIndex(collection))
{
    Console.Write("Index=" + item.Index);
    Console.Write(";Value= " + item.Value);
    Console.Write(";IsLast=" + item.IsLast);
    Console.WriteLine();
}

Hier ist der Code für ForEachHelper Klasse.

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

Es ist nichts Falsches daran, eine Zählervariable zu verwenden.In der Tat, ob Sie verwenden for, foreach while oder do, muss irgendwo eine Zählervariable deklariert und erhöht werden.

Verwenden Sie also diese Redewendung, wenn Sie nicht sicher sind, ob Sie über eine entsprechend indizierte Sammlung verfügen:

var i = 0;
foreach (var e in collection) {
   // Do stuff with 'e' and 'i'
   i++;
}

Andernfalls verwenden Sie dieses, wenn Sie möchten wissen dass Ihr indexierbar Die Sammlung ist O(1) für den Indexzugriff (für den sie bestimmt sein wird). Array und wahrscheinlich für List<T> (Die Dokumentation sagt es nicht), aber nicht unbedingt für andere Typen (z. B 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'
}

Es sollte niemals notwendig sein, das Gerät „manuell“ zu bedienen IEnumerator durch Anrufung MoveNext() und verhören Current - foreach erspart Ihnen diese besondere Mühe ...Wenn Sie Elemente überspringen müssen, verwenden Sie einfach a continue im Körper der Schleife.

Und nur der Vollständigkeit halber, je nachdem, was Sie waren tun Mit Ihrem Index (die oben genannten Konstrukte bieten viel Flexibilität) können Sie Parallel LINQ verwenden:

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

Wir gebrauchen AsParallel() oben, weil es bereits 2014 ist und wir diese mehreren Kerne sinnvoll nutzen wollen, um die Dinge zu beschleunigen.Darüber hinaus gilt für „sequentielles“ LINQ: Du bekommst nur eine ForEach() Erweiterungsmethode ein List<T> Und Array ...und es ist nicht klar, ob die Verwendung besser ist als die einfache Ausführung foreach, da Sie wegen der hässlicheren Syntax immer noch Single-Threaded ausführen.

Hier ist eine Lösung, die ich gerade für dieses Problem gefunden habe

Originalcode:

int index=0;
foreach (var item in enumerable)
{
    blah(item, index); // some code that depends on the index
    index++;
}

Aktualisierter Code

enumerable.ForEach((item, index) => blah(item, index));

Erweiterungsmethode:

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

Dies würde für unterstützende Sammlungen funktionieren IList.

C# 7 bietet uns endlich eine elegante Möglichkeit, dies zu tun:

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

Es funktioniert nur für eine Liste und nicht für IEnumerable, aber in LINQ gibt es Folgendes:

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 Ich habe nicht gesagt, dass es eine tolle Antwort war, ich habe nur gesagt, dass es nur zeigt, dass es möglich ist, das zu tun, was er verlangt hat :)

@Graphain Ich würde nicht erwarten, dass es schnell geht – ich bin mir nicht ganz sicher, wie es funktioniert, es könnte jedes Mal die gesamte Liste durchgehen, um ein passendes Objekt zu finden, was eine Menge Vergleiche bedeuten würde.

Das heißt, List könnte zusammen mit der Anzahl einen Index jedes Objekts führen.

Jonathan scheint eine bessere Idee zu haben, wenn er das näher erläutern würde?

Es wäre jedoch besser, einfach zu zählen, wo Sie in der Foreach gerade sind, einfacher und anpassungsfähiger.

Fügen Sie einfach Ihren eigenen Index hinzu.Halte es einfach.

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

So mache ich es, was wegen seiner Einfachheit/Kürze schön ist, aber wenn man viel im Schleifenkörper macht obj.Value, es wird ziemlich schnell alt werden.

foreach(var obj in collection.Select((item, index) => new { Index = index, Value = item }) {
    string foo = string.Format("Something[{0}] = {1}", obj.Index, obj.Value);
    ...
}

Warum foreach?!

Der einfachste Weg ist die Verwendung für statt foreach wenn Sie List verwenden .

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

ODER wenn Sie foreach verwenden möchten:

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

Sie können dies verwenden, um den Index jeder Schleife zu ermitteln:

myList.indexOf(m)

Verwenden Sie besser ein Schlüsselwort continue sichere Konstruktion wie diese

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

Wenn die Sammlung eine Liste ist, können Sie List.IndexOf verwenden, wie in:

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

In der führenden Antwort heißt es:

„Offensichtlich ist das Konzept eines Index dem Konzept der Aufzählung fremd und kann nicht umgesetzt werden.“

Dies trifft zwar auf die aktuelle C#-Version zu, stellt jedoch keine konzeptionelle Grenze dar.

Die Schaffung einer neuen C#-Sprachfunktion durch MS könnte dieses Problem lösen, zusammen mit der Unterstützung einer neuen Schnittstelle 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);
}

Wenn foreach ein IEnumerable übergeben wird und ein IIndexedEnumerable nicht aufgelöst werden kann, es aber mit einem var-Index abgefragt wird, kann der C#-Compiler die Quelle mit einem IndexedEnumerable-Objekt umschließen, das den Code zum Nachverfolgen des Index hinzufügt.

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

Warum:

  • Foreach sieht besser aus und stellt in Geschäftsanwendungen selten einen Leistungsengpass dar
  • Foreach kann den Speicher effizienter nutzen.Eine Pipeline von Funktionen haben, anstatt bei jedem Schritt in neue Sammlungen zu konvertieren.Wen kümmert es, wenn es ein paar CPU-Zyklen mehr verbraucht, wenn es weniger CPU-Cache-Fehler und weniger GC.Collects gibt?
  • Wenn der Codierer den Index-Tracking-Code hinzufügen muss, wird die Schönheit beeinträchtigt
  • Es ist recht einfach zu implementieren (danke MS) und abwärtskompatibel

Obwohl die meisten Leute hier keine MS sind, ist dies eine korrekte Antwort, und Sie können sich bei MS dafür einsetzen, eine solche Funktion hinzuzufügen.Sie könnten bereits Ihren eigenen Iterator mit erstellen Erweiterungsfunktion und Verwendung von Tupeln, aber MS könnte den syntaktischen Zucker streuen, um die Erweiterungsfunktion zu vermeiden

Meine Lösung für dieses Problem ist eine Erweiterungsmethode WithIndex(),

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

Benutze es gerne

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

Aus Interesse hat Phil Haack gerade ein Beispiel dafür im Zusammenhang mit einem Razor Templated Delegate geschrieben (http://haacked.com/archive/2011/04/14/a-better-razor-foreach-loop.aspx)

Tatsächlich schreibt er eine Erweiterungsmethode, die die Iteration in eine „IteratedItem“-Klasse einschließt (siehe unten) und so den Zugriff auf den Index und das Element während der Iteration ermöglicht.

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

Dies wäre jedoch in einer Nicht-Razor-Umgebung in Ordnung, wenn Sie einen einzelnen Vorgang ausführen (d. h.(eines, das als Lambda bereitgestellt werden könnte) wird in Nicht-Razor-Kontexten kein solider Ersatz für die for/foreach-Syntax sein.

Ich denke nicht, dass das sehr effizient sein sollte, aber es funktioniert:

@foreach (var banner in Model.MainBanners) {
    @Model.MainBanners.IndexOf(banner)
}

Das habe ich eingebaut 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.

Sie können es auch einfach verwenden string.join:

var joinResult = string.Join(",", listOfNames);

Sie können Ihre Schleife folgendermaßen schreiben:

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

Nach dem Hinzufügen der folgenden Struktur- und Erweiterungsmethode.

Die Struktur- und Erweiterungsmethode kapselt die Enumerable.Select-Funktionalität.

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

Ich glaube nicht, dass es eine Möglichkeit gibt, den Wert der aktuellen Iteration einer foreach-Schleife zu ermitteln.Sich selbst zu zählen, scheint der beste Weg zu sein.

Darf ich fragen, warum Sie das wissen wollen?

Es scheint, dass Sie höchstwahrscheinlich eines von drei Dingen tun würden:

1) Holen Sie sich das Objekt aus der Sammlung, aber in diesem Fall haben Sie es bereits.

2) Zählen der Objekte für die spätere Nachbearbeitung ... Die Sammlungen verfügen über eine Count-Eigenschaft, die Sie nutzen können.

3) Festlegen einer Eigenschaft für das Objekt basierend auf seiner Reihenfolge in der Schleife ... obwohl Sie dies problemlos festlegen könnten, wenn Sie das Objekt zur Sammlung hinzugefügt haben.

Sofern Ihre Sammlung den Index des Objekts nicht über eine Methode zurückgeben kann, besteht die einzige Möglichkeit darin, einen Zähler wie in Ihrem Beispiel zu verwenden.

Bei der Arbeit mit Indizes besteht die einzig sinnvolle Lösung des Problems jedoch in der Verwendung einer for-Schleife.Alles andere führt zu Codekomplexität, ganz zu schweigen von der zeitlichen und räumlichen Komplexität.

Wie wäre es mit so etwas?Beachten Sie, dass myDelimitedString möglicherweise null ist, wenn myEnumerable leer ist.

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

Ich hatte gerade dieses Problem, aber das Nachdenken über das Problem in meinem Fall ergab die beste Lösung, die nichts mit der erwarteten Lösung zu tun hatte.

Es könnte ein recht häufiger Fall sein, im Grunde lese ich aus einer Quellliste und erstelle darauf basierende Objekte in einer Zielliste. Allerdings muss ich zuerst prüfen, ob die Quellelemente gültig sind und möchte die Zeile eines beliebigen zurückgeben Fehler.Auf den ersten Blick möchte ich den Index in den Enumerator des Objekts an der Current-Eigenschaft übernehmen. Da ich diese Elemente jedoch kopiere, kenne ich implizit ohnehin den aktuellen Index vom aktuellen Ziel.Offensichtlich hängt es von Ihrem Zielobjekt ab, aber für mich war es eine Liste, und höchstwahrscheinlich wird sie ICollection implementieren.

d.h.

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

Nicht immer zutreffend, aber oft genug, um erwähnenswert zu sein, denke ich.

Wie auch immer, der Punkt ist, dass es manchmal bereits eine nicht offensichtliche Lösung in der Logik gibt, die Sie haben ...

Ich war mir aufgrund der Frage nicht sicher, was Sie mit den Indexinformationen machen wollten.In C# können Sie jedoch normalerweise die IEnumerable.Select-Methode anpassen, um den Index nach Ihren Wünschen zu erhalten.Ich könnte zum Beispiel so etwas verwenden, um anzugeben, ob ein Wert gerade oder ungerade ist.

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

Dadurch erhalten Sie ein Wörterbuch mit Namen, das anzeigt, ob das Element in der Liste ungerade (1) oder gerade (0) war.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top