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

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

  •  09-06-2019
  •  | 
  •  

Pregunta

¿Existe alguna construcción de lenguaje poco común que no haya encontrado (como las pocas que aprendí recientemente, algunas en Stack Overflow) en C# para obtener un valor que represente la iteración actual de un bucle foreach?

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

int i = 0;
foreach (Object o in collection)
{
    // ...
    i++;
}
¿Fue útil?

Solución

El 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:

  • MoverSiguiente()
  • Actual

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

El concepto de índice es ajeno al concepto de enumeración y no se puede realizar.

Por eso, la mayoría de las colecciones se pueden recorrer utilizando un indexador y la construcción de bucle for.

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

Otros consejos

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 Seleccionar] representa el índice del elemento fuente.

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

Las asignaciones de montón se pueden evitar usando ValueTuple si estás usando C# 7.0 o posterior:

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

También puedes eliminar el item. mediante el uso de desestructuración automática:

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

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

Finalmente C#7 tiene una sintaxis decente para obtener un índice dentro de un foreach bucle (i.mi.tuplas):

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

Se necesitaría un pequeño método de extensión:

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

No estoy de acuerdo con los comentarios de 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 una for bucle en todas las circunstancias.

Por ejemplo, si tienes un Lector de datos 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).Por lo tanto, esto es más seguro ya que evita fugas de conexión incluso si olvida cerrar el lector.

(Por supuesto, es una buena práctica cerrar siempre los lectores, pero el compilador no lo detectará si no lo hace; no puede garantizar que haya cerrado todos los lectores, pero puede aumentar la probabilidad de que no se pierdan las conexiones obteniendo en el hábito de usar foreach.)

Puede haber otros ejemplos del llamado implícito de la Dispose método es útil.

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

Solo necesita usar la sobrecarga de indexación de Select para envolver cada elemento de la colección con un objeto anónimo que conoce el índice.Esto se puede hacer con 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);
}

Usando 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 enumerador usando GetEnumerator y luego haces un bucle usando un for bucle.Sin embargo, el truco consiste en hacer que la condición del bucle listEnumerator.MoveNext() == true.

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

Usando LINQ, C# 7 y el System.ValueTuple Paquete NuGet, puedes hacer esto:

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

Puedes usar el regular foreach construir y poder acceder al valor y al índice directamente, no como miembro de un objeto, y mantiene ambos campos solo en el alcance del bucle.Por estas razones, creo que esta es la mejor solución si puedes usar C# 7 y System.ValueTuple.

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

No hay nada de malo en usar una variable de contador.De hecho, ya sea que uses for, foreach while o do, se debe declarar e incrementar una variable de contador en algún lugar.

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

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

De lo contrario, usa 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 lo 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' el IEnumerator invocando MoveNext() e interrogando Current - foreach te está ahorrando esa molestia en particular...Si necesita omitir elementos, simplemente use un continue en el cuerpo del bucle.

Y solo para completar, dependiendo de lo que eras haciendo 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 estamos en 2014 y queremos hacer un buen uso de esos múltiples núcleos 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 solo subproceso para una sintaxis más fea.

Aquí hay una solución que se me ocurrió 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;
    }
int index;
foreach (Object o in collection)
{
    index = collection.indexOf(o);
}

Esto funcionaría para colecciones que soportan IList.

C# 7 finalmente nos brinda una manera elegante de hacer esto:

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

Sólo funcionará para una Lista y no para cualquier IEnumerable, pero en LINQ existe esto:

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 No dije que fuera una gran respuesta, solo dije que solo demostraba que era posible hacer lo que me pidió :)

@Graphain No esperaría que fuera rápido; no estoy del todo seguro de cómo funciona, podría reiterar toda la lista cada vez para encontrar un objeto coincidente, lo que sería un montón de comparaciones.

Dicho esto, List podría mantener un índice de cada objeto junto con el recuento.

Jonathan parece tener una idea mejor, ¿podría dar más detalles?

Sin embargo, sería mejor simplemente llevar un recuento de dónde estás haciendo en el foreach, ya que es más simple y más adaptable.

Simplemente agregue su propio índice.Mantenlo simple.

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

Así es como lo hago, lo cual es bueno por su simplicidad/brevedad, pero si estás haciendo mucho en el cuerpo del bucle obj.Value, envejecerá bastante rápido.

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

¡¿Por qué para cada uno?!

La forma más sencilla es utilizar para en lugar de para cada uno si estás usando Lista .

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

O si quieres usar foreach:

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

Puedes usar esto para saber el índice de cada bucle:

myList.indexOf(m)

Mejor usar palabra clave continue construcción segura como esta

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

Si la colección es una lista, puedes usar List.IndexOf, como en:

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

La respuesta principal dice:

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

Si bien esto es cierto para la versión actual de C#, no es un límite conceptual.

La creación de una nueva característica del lenguaje C# por parte de MS podría resolver este problema, junto con el soporte para una nueva interfaz 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);
}

Si a foreach se le pasa un IEnumerable y no puede resolver un IIndexedEnumerable, pero se solicita con un índice var, entonces el compilador de C# puede empaquetar el código fuente con un objeto IndexedEnumerable, que agrega el código para rastrear el índice.

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

Por qué:

  • Foreach se ve mejor y en aplicaciones empresariales rara vez es un cuello de botella en el rendimiento.
  • Foreach puede ser más eficiente con la memoria.Tener una canalización de funciones en lugar de convertirlas en nuevas colecciones en cada paso.¿A quién le importa si usa algunos ciclos de CPU más, si hay menos fallas de caché de CPU y menos GC.Collects?
  • Exigir que el codificador agregue un código de seguimiento de índice arruina la belleza
  • Es bastante fácil de implementar (gracias MS) y es compatible con versiones anteriores.

Si bien la mayoría de las personas aquí no son MS, esta es una respuesta correcta y puede presionar a MS para que agregue dicha característica.Ya podrías construir tu propio iterador con un función de extensión y uso de tuplas, pero MS podría espolvorear azúcar sintáctico para evitar la función de extensión

Mi solución para este problema es un método de extensión. WithIndex(),

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

Úselo como

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

Por interés, Phil Haack acaba de escribir un ejemplo de esto en el contexto de un Delegado con plantilla Razor (http://haacked.com/archive/2011/04/14/a-better-razor-foreach-loop.aspx)

Efectivamente, escribe un método de extensión que envuelve la iteración en una clase "IteratedItem" (ver más abajo) permitiendo el acceso al índice y al elemento durante la iteración.

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

Sin embargo, si bien esto estaría bien en un entorno que no sea Razor si está realizando una sola operación (es decir,(uno que podría proporcionarse como lambda) no será un reemplazo sólido de la sintaxis for/foreach en contextos que no sean Razor.

No creo que esto deba ser muy eficiente, pero funciona:

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

Construí esto en 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.

También podrías usar string.join:

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

Puedes escribir tu bucle así:

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

Después de agregar la siguiente estructura y método de extensión.

El método de estructura y extensión encapsula la funcionalidad 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);
    }
}

No creo que haya una manera de obtener el valor de la iteración actual de un bucle foreach.Contarte a ti mismo parece ser la mejor manera.

¿Puedo preguntar por qué querrías saberlo?

Parece que lo más probable es que estés haciendo una de estas tres cosas:

1) Obtener el objeto de la colección, pero en este caso ya lo tienes.

2) Contar los objetos para su posterior procesamiento posterior... las colecciones tienen una propiedad Count que puede utilizar.

3) Establecer una propiedad en el objeto en función de su orden en el bucle... aunque podría configurarla fácilmente cuando agregue el objeto a la colección.

A menos que su colección pueda devolver el índice del objeto mediante algún método, la única forma es utilizar un contador como en su ejemplo.

Sin embargo, cuando se trabaja con índices, la única respuesta razonable al problema es utilizar un bucle for.Cualquier otra cosa introduce complejidad en el código, sin mencionar la complejidad del tiempo y el espacio.

¿Qué tal algo como esto?Tenga en cuenta que myDelimitedString puede ser nulo si myEnumerable está vacío.

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

Acabo de tener este problema, pero pensar en el problema en mi caso me dio la mejor solución, sin relación con la solución esperada.

Podría ser un caso bastante común, básicamente, estoy leyendo de una lista de origen y creando objetos basados ​​en ellos en una lista de destino, sin embargo, primero tengo que verificar si los elementos de origen son válidos y quiero devolver la fila de cualquier error.A primera vista, quiero obtener el índice en el enumerador del objeto en la propiedad Actual; sin embargo, a medida que copio estos elementos, de todos modos conozco implícitamente el índice actual desde el destino actual.Obviamente depende de su objeto de destino, pero para mí era una Lista y lo más probable es que implemente ICollection.

es decir.

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

No siempre es aplicable, pero creo que es lo suficientemente frecuente como para que valga la pena mencionarlo.

De todos modos, el punto es que a veces ya hay una solución no obvia en la lógica que tienes...

No estaba seguro de qué intentaba hacer con la información del índice según la pregunta.Sin embargo, en C#, normalmente puedes adaptar el método IEnumerable.Select para obtener el índice de lo que quieras.Por ejemplo, podría usar algo como esto para determinar si un valor es par o impar.

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

Esto le daría un diccionario por nombre indicando si el elemento era impar (1) o par (0) en la lista.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top