Domanda

Esiste qualche raro costrutto linguistico che non ho incontrato (come i pochi che ho imparato di recente, alcuni su Stack Overflow) in C# per ottenere un valore che rappresenta l'iterazione corrente di un ciclo foreach?

Ad esempio, attualmente faccio qualcosa del genere a seconda delle circostanze:

int i = 0;
foreach (Object o in collection)
{
    // ...
    i++;
}
È stato utile?

Soluzione

IL foreach serve per scorrere le raccolte che implementano IEnumerable.Lo fa chiamando GetEnumerator sulla raccolta, che restituirà un file Enumerator.

Questo enumeratore ha un metodo e una proprietà:

  • SpostaAvanti()
  • Attuale

Current restituisce l'oggetto su cui si trova attualmente l'Enumerator, MoveNext aggiornamenti Current all'oggetto successivo.

Il concetto di indice è estraneo al concetto di enumerazione e non può essere eseguito.

Per questo motivo, la maggior parte delle raccolte può essere attraversata utilizzando un indicizzatore e il costrutto del ciclo for.

Preferisco di gran lunga utilizzare un ciclo for in questa situazione rispetto al monitoraggio dell'indice con una variabile locale.

Altri suggerimenti

Ian Mercer ha pubblicato una soluzione simile a questa Il blog di Phil Haack:

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

Questo ti dà l'oggetto (item.value) e il suo indice (item.i) usando questo sovraccarico di Linq Select:

il secondo parametro della funzione [all'interno di Select] rappresenta l'indice dell'elemento sorgente.

IL new { i, value } ne sta creando uno nuovo oggetto anonimo.

Le allocazioni di heap possono essere evitate utilizzando ValueTuple se utilizzi C# 7.0 o versione successiva:

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

Puoi anche eliminare il item. utilizzando la destrutturazione automatica:

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

Potrebbe fare qualcosa del genere:

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

Finalmente C#7 ha una sintassi decente per ottenere un indice all'interno di a foreach ciclo (es.e.tuple):

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

Sarebbe necessario un piccolo metodo di estensione:

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

Non sono d'accordo con i commenti secondo cui a for loop è una scelta migliore nella maggior parte dei casi.

foreach è un costrutto utile e non sostituibile da a for loop in ogni circostanza.

Ad esempio, se hai a Lettore di dati e scorrere tutti i record utilizzando a foreach chiama automaticamente il Smaltire metodo e chiude il lettore (che può quindi chiudere automaticamente la connessione).Ciò è quindi più sicuro poiché impedisce perdite di connessione anche se ci si dimentica di chiudere il lettore.

(Certamente è buona pratica chiudere sempre i lettori ma il compilatore non lo rileverà se non lo fai - non puoi garantire di aver chiuso tutti i lettori ma puoi rendere più probabile che non si perderanno le connessioni ottenendo nell'abitudine di usare foreach.)

Potrebbero esserci altri esempi della chiamata implicita del Dispose il metodo sia utile.

Risposta letterale: attenzione, le prestazioni potrebbero non essere buone quanto quelle ottenute utilizzando semplicemente un file int per tenere traccia dell'indice.Almeno è meglio che usare IndexOf.

Devi solo utilizzare l'overload di indicizzazione di Select per racchiudere ogni elemento nella raccolta con un oggetto anonimo che conosca l'indice.Questa operazione può essere eseguita contro qualsiasi cosa che implementi 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);
}

Usando la risposta di @FlySwat, ho trovato questa soluzione:

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

Ottieni l'enumeratore utilizzando GetEnumerator e poi esegui il loop utilizzando a for ciclo continuo.Tuttavia, il trucco sta nel creare la condizione del ciclo listEnumerator.MoveNext() == true.

Dal momento che MoveNext Il metodo di un enumeratore restituisce true se c'è un elemento successivo ed è possibile accedervi, facendo in modo che la condizione del ciclo faccia interrompere il ciclo quando finiamo gli elementi su cui eseguire l'iterazione.

Utilizzando LINQ, C# 7 e System.ValueTuple pacchetto NuGet, puoi farlo:

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

Puoi usare quello normale foreach costruire ed essere in grado di accedere direttamente al valore e all'indice, non come membro di un oggetto, e mantiene entrambi i campi solo nell'ambito del ciclo.Per questi motivi, credo che questa sia la soluzione migliore se sei in grado di utilizzare C# 7 e System.ValueTuple.

Potresti racchiudere l'enumeratore originale con un altro che contenga le informazioni sull'indice.

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

Ecco il codice per il ForEachHelper classe.

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

Non c'è niente di sbagliato nell'usare una variabile contatore.In effetti, sia che tu usi for, foreach while O do, una variabile contatore deve essere dichiarata e incrementata da qualche parte.

Quindi usa questo modo di dire se non sei sicuro di avere una raccolta adeguatamente indicizzata:

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

Altrimenti usa questo se tu Sapere che il tuo indicizzabile collection è O(1) per l'accesso all'indice (che sarà per Array e probabilmente per List<T> (la documentazione non lo dice), ma non necessariamente per altri tipi (come 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'
}

Non dovrebbe mai essere necessario azionare manualmente il IEnumerator invocando MoveNext() e interrogatorio Current - foreach ti sta risparmiando quel fastidio particolare...se devi saltare gli elementi, usa semplicemente a continue nel corpo del ciclo.

E solo per completezza, a seconda di cosa eri facendo con il tuo indice (i costrutti di cui sopra offrono molta flessibilità), potresti utilizzare 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));

Noi usiamo AsParallel() sopra, perché siamo già nel 2014 e vogliamo fare buon uso di questi core multipli per accelerare le cose.Inoltre, per LINQ "sequenziale", ottieni solo un ForEach() metodo di estensione attivo List<T> E Array ...e non è chiaro se usarlo sia meglio che fare un semplice foreach, dal momento che stai ancora eseguendo il thread singolo per una sintassi più brutta.

Ecco una soluzione che ho appena trovato per questo problema

Codice originale:

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

Codice aggiornato

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

Metodo di estensione:

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

Ciò funzionerebbe per il supporto delle raccolte IList.

C# 7 finalmente ci offre un modo elegante per farlo:

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

Funzionerà solo per un List e non per un IEnumerable, ma in LINQ c'è questo:

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 Non ho detto che fosse un'ottima risposta, ho solo detto che stava solo dimostrando che era possibile fare quello che aveva chiesto :)

@Graphain Non mi aspetto che sia veloce: non sono del tutto sicuro di come funzioni, potrebbe ripetere ogni volta l'intero elenco per trovare un oggetto corrispondente, il che sarebbe un sacco di confronti.

Detto questo, List potrebbe mantenere un indice di ciascun oggetto insieme al conteggio.

Jonathan sembra avere un'idea migliore, se la elaborasse?

Sarebbe meglio tenere semplicemente il conto di dove ti trovi nel foreach, più semplice e più adattabile.

Aggiungi semplicemente il tuo indice.Mantienilo semplice.

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

Ecco come lo faccio, il che è carino per la sua semplicità/brevità, ma se stai facendo molto nel loop body obj.Value, invecchierà abbastanza velocemente.

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

Perché ciascuno?!

Il modo più semplice è utilizzare per invece di perciascuno se stai usando List .

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

OPPURE se vuoi usare foreach:

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

puoi usarlo per conoscere l'indice di ciascun loop:

myList.indexOf(m)

Meglio usare le parole chiave continue costruzione sicura come questa

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

Se la raccolta è un elenco, puoi utilizzare List.IndexOf, come in:

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

La risposta principale afferma:

"Ovviamente il concetto di indice è estraneo al concetto di enumerazione e non può essere fatto."

Sebbene ciò sia vero per la versione corrente di C#, non si tratta di un limite concettuale.

La creazione di una nuova funzionalità del linguaggio C# da parte di MS potrebbe risolvere questo problema, insieme al supporto per una nuova interfaccia 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);
}

Se a foreach viene passato un IEnumerable e non è possibile risolvere un IIndexedEnumerable, ma viene richiesto con var index, il compilatore C# può racchiudere l'origine con un oggetto IndexedEnumerable, che aggiunge il codice per tenere traccia dell'indice.

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

Perché:

  • Foreach ha un aspetto migliore e nelle applicazioni aziendali raramente rappresenta un collo di bottiglia nelle prestazioni
  • Foreach può essere più efficiente sulla memoria.Avere una pipeline di funzioni invece di convertirle in nuove raccolte ad ogni passaggio.A chi importa se utilizza qualche ciclo in più della CPU, se ci sono meno errori di cache della CPU e meno GC.Collects
  • Richiedere al programmatore di aggiungere il codice di monitoraggio dell'indice rovina la bellezza
  • È abbastanza facile da implementare (grazie MS) ed è compatibile con le versioni precedenti

Sebbene la maggior parte delle persone qui non siano SM, questa è una risposta corretta e puoi fare pressione sulla SM per aggiungere tale funzionalità.Potresti già creare il tuo iteratore con un file funzione di estensione e utilizzare tuple, ma MS potrebbe spargere zucchero sintattico per evitare la funzione di estensione

La mia soluzione a questo problema è un metodo di estensione WithIndex(),

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

Usalo come

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

Per interesse, Phil Haack ha appena scritto un esempio di questo nel contesto di un Razor Templated Delegate (http://haacked.com/archive/2011/04/14/a-better-razor-foreach-loop.aspx)

In effetti scrive un metodo di estensione che avvolge l'iterazione in una classe "IteratedItem" (vedi sotto) consentendo l'accesso all'indice e all'elemento durante l'iterazione.

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

Tuttavia, anche se ciò andrebbe bene in un ambiente non Razor se si esegue una singola operazione (ad esempiouno che potrebbe essere fornito come lambda) non sarà un valido sostituto della sintassi for/foreach in contesti non Razor.

Non penso che questo dovrebbe essere abbastanza efficiente, ma funziona:

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

Ho integrato questo 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.

Potresti anche semplicemente usare string.join:

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

Puoi scrivere il tuo ciclo in questo modo:

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

Dopo aver aggiunto la seguente struttura e metodo di estensione.

Il metodo struct e extension incapsulano la funzionalità 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);
    }
}

Non credo che ci sia un modo per ottenere il valore dell'iterazione corrente di un ciclo foreach.Contare te stesso sembra essere il modo migliore.

Posso chiederti perché vorresti saperlo?

Sembra che molto probabilmente faresti una delle tre cose:

1) Ottenere l'oggetto dalla collezione, ma in questo caso ce l'hai già.

2) Conteggio degli oggetti per la successiva post-elaborazione... le raccolte hanno una proprietà Count che potresti utilizzare.

3) Impostazione di una proprietà sull'oggetto in base al suo ordine nel ciclo... anche se potresti facilmente impostarla quando hai aggiunto l'oggetto alla raccolta.

A meno che la tua raccolta non possa restituire l'indice dell'oggetto tramite qualche metodo, l'unico modo è utilizzare un contatore come nel tuo esempio.

Tuttavia, quando si lavora con gli indici, l'unica risposta ragionevole al problema è utilizzare un ciclo for.Qualsiasi altra cosa introduce complessità del codice, per non parlare della complessità temporale e spaziale.

Che ne dici di qualcosa del genere?Tieni presente che myDelimitedString potrebbe essere null se myEnumerable è vuoto.

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

Ho appena avuto questo problema, ma pensare al problema nel mio caso ha fornito la soluzione migliore, estranea alla soluzione prevista.

Potrebbe essere un caso abbastanza comune, fondamentalmente sto leggendo da un elenco di origine e creando oggetti basati su di essi in un elenco di destinazione, tuttavia, devo prima verificare se gli elementi di origine sono validi e voglio restituire la riga di qualsiasi errore.A prima vista, voglio inserire l'indice nell'enumeratore dell'oggetto nella proprietà Current, tuttavia, poiché copio questi elementi, conosco comunque implicitamente l'indice corrente dalla destinazione corrente.Ovviamente dipende dall'oggetto di destinazione, ma per me era una List, e molto probabilmente implementerà ICollection.

cioè.

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

Non sempre applicabile, ma abbastanza spesso da meritare di essere menzionato, credo.

Comunque, il punto è che a volte esiste già una soluzione non ovvia nella logica che hai...

Non ero sicuro di cosa stavi cercando di fare con le informazioni dell'indice in base alla domanda.Tuttavia, in C#, in genere è possibile adattare il metodo IEnumerable.Select per ottenere l'indice da ciò che si desidera.Ad esempio, potrei usare qualcosa di simile per stabilire se un valore è pari o dispari.

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

Questo ti fornirebbe un dizionario per nome che indica se l'elemento era dispari (1) o pari (0) nell'elenco.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top