Pregunta ¿Cómo se obtiene el índice de la iteración actual de un ciclo foreach?


¿Hay alguna construcción de lenguaje raro que no he encontrado (como los pocos que he aprendido recientemente, algunos en Desbordamiento de pila) en C # para obtener un valor que representa la iteración actual de un ciclo foreach?

Por ejemplo, actualmente hago algo como esto dependiendo de las circunstancias:

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

669
2017-09-04 01:38


origen


Respuestas:


los foreach es para iterar sobre colecciones que implementan IEnumerable. Lo hace llamando GetEnumerator en la colección, que devolverá un Enumerator.

Este enumerador tiene un método y una propiedad:

  • MoveNext ()
  • Corriente

Current devuelve el objeto en el que el Enumerador se encuentra actualmente, MoveNext actualizaciones Current al siguiente objeto.

Obviamente, el concepto de índice es ajeno al concepto de enumeración y no se puede hacer.

Debido a eso, la mayoría de las colecciones pueden atravesarse usando un indexador y la construcción for loop.

Prefiero usar un bucle for en esta situación, en comparación con el seguimiento del índice con una variable local.


433
2017-09-04 01:46



Ian Mercer publicó una solución similar a esta en El blog de Phil Haack:

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

Esto te da el artículo (item.value) y su índice (item.i) mediante el uso esta sobrecarga de Linq Select:

el segundo parámetro de la función [dentro de Select] representa el índice del elemento fuente.

los new { i, value } está creando un nuevo objeto anónimo.


441
2017-07-11 16:52



Podría hacer algo como esto:

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

98
2017-09-04 01:49



No estoy de acuerdo con los comentarios que for loop es una mejor opción en la mayoría de los casos.

foreach es una construcción útil, y no reemplazable por un for bucle en todas las circunstancias.

Por ejemplo, si tienes un DataReader y recorrer todos los registros usando un foreach llama automáticamente al Disponer método y cierra el lector (que luego puede cerrar la conexión automáticamente). Esto es, por lo tanto, más seguro ya que evita pérdidas de conexión incluso si olvida cerrar el lector.

(Claro que es una buena práctica cerrar siempre a los lectores, pero el compilador no lo captará si no lo hace; no puede garantizar que haya cerrado a todos los lectores, pero puede hacer que sea más probable que no se filtren las conexiones al obtener en el hábito de usar foreach).

Puede haber otros ejemplos de la llamada implícita del Dispose método siendo útil.


77
2018-06-25 23:49



Respuesta literal: advertencia, el rendimiento puede no ser tan bueno como solo usar un int para rastrear el índice. Al menos es mejor que usar IndexOf.

Solo necesita utilizar la sobrecarga de indexación de Seleccionar para ajustar cada elemento de la colección con un objeto anónimo que conozca el índice. Esto se puede hacer contra cualquier cosa que implemente 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);
}

54
2017-09-16 21:50



Finalmente C # 7 tiene una sintaxis decente para obtener un índice dentro de un foreach loop (es decir, tuplas):

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

Un pequeño método de extensión sería necesario:

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

40
2017-10-12 11:12



Utilizando la respuesta de @ FlySwat, se me ocurrió esta solución:

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

Obtienes el uso del enumerador GetEnumerator y luego haces un bucle usando un forlazo. Sin embargo, el truco es hacer que la condición del bucle listEnumerator.MoveNext() == true.

Desde el MoveNext El método de un enumerador devuelve verdadero si hay un elemento siguiente y se puede acceder, haciendo que la condición de bucle haga que el bucle se detenga cuando nos quedemos sin elementos para iterar.


32
2017-10-07 22:49



Puede ajustar el enumerador original con otro que contenga la información del índice.

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

Aquí está el código para el ForEachHelper clase.

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

21
2017-07-20 19:15



Aquí hay una solución que acabo de presentar para este problema

Código original:

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

Código actualizado

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

Método de extensión:

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

16
2017-12-31 05:26



No hay nada malo con el uso de una variable de contador. De hecho, ya sea que uses for, foreach  while o do, una variable de contador debe declararse e incrementarse en alguna parte.

Así que usa este modismo si no estás seguro de si tienes una colección indexada adecuadamente:

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

De lo contrario, use este si saber Que tu indexable la colección es O (1) para el acceso al índice (que será para Array y probablemente para List<T> (la documentación no dice), pero no necesariamente para otros tipos (como 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'
}

Nunca debería ser necesario "operar" manualmente IEnumerator invocando MoveNext() e interrogando Current - foreach te está salvando esa molestia en particular ... si necesitas saltear artículos, solo usa un continue en el cuerpo del bucle

Y para completar, dependiendo de lo que eras obra con su índice (las construcciones anteriores ofrecen mucha flexibilidad), puede usar 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));

Usamos AsParallel() arriba, porque ya es 2014, y queremos hacer un buen uso de esos núcleos múltiples para acelerar las cosas. Además, para LINQ 'secuencial', solo obtienes un ForEach() método de extensión en List<T> y Array ... y no está claro que usarlo sea mejor que hacer un simple foreach, ya que todavía está ejecutando un único subproceso para una sintaxis más fea.


14
2018-02-25 14:07