Pergunta

Existe alguma construção de linguagem rara que não encontrei (como as poucas que aprendi recentemente, algumas no Stack Overflow) em C# para obter um valor que representa a iteração atual de um loop foreach?

Por exemplo, atualmente faço algo assim dependendo das circunstâncias:

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

Solução

O foreach é para iterar sobre coleções que implementam IEnumerable.Ele faz isso ligando GetEnumerator na coleção, que retornará um Enumerator.

Este Enumerador possui um método e uma propriedade:

  • MoverPróximo()
  • Atual

Current retorna o objeto em que o Enumerator está atualmente, MoveNext atualizações Current para o próximo objeto.

O conceito de índice é estranho ao conceito de enumeração e não pode ser feito.

Por causa disso, a maioria das coleções pode ser percorrida usando um indexador e a construção do loop for.

Eu prefiro usar um loop for nesta situação em vez de rastrear o índice com uma variável local.

Outras dicas

Ian Mercer postou uma solução semelhante a esta em Blog de Phil Haack:

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

Isso lhe dá o item (item.value) e seu índice (item.i) usando essa sobrecarga do Linq Select:

o segundo parâmetro da função [dentro do Select] representa o índice do elemento de origem.

O new { i, value } está criando um novo objeto anônimo.

Alocações de heap podem ser evitadas usando ValueTuple se você estiver usando C# 7.0 ou posterior:

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

Você também pode eliminar o item. usando desestruturação automática:

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

Poderia fazer algo assim:

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, o C#7 tem uma sintaxe decente para obter um índice dentro de um foreach laço (ou.e.tuplas):

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

Seria necessário um pequeno método de extensão:

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

Discordo dos comentários de que um for loop é uma escolha melhor na maioria dos casos.

foreach é uma construção útil e não substituível por um for loop em todas as circunstâncias.

Por exemplo, se você tiver um Leitor de dados e percorrer todos os registros usando um foreach ele chama automaticamente o Descartar método e fecha o leitor (que pode então fechar a conexão automaticamente).Isto é, portanto, mais seguro, pois evita vazamentos de conexão mesmo se você esquecer de fechar o leitor.

(Claro que é uma boa prática sempre fechar os leitores, mas o compilador não irá capturá-lo se você não fizer isso - você não pode garantir que fechou todos os leitores, mas pode aumentar a probabilidade de não vazar conexões obtendo tenho o hábito de usar foreach.)

Pode haver outros exemplos da chamada implícita do Dispose método sendo útil.

Resposta literal - aviso, o desempenho pode não ser tão bom quanto apenas usar um int para rastrear o índice.Pelo menos é melhor do que usar IndexOf.

Você só precisa usar a sobrecarga de indexação do Select para agrupar cada item da coleção com um objeto anônimo que conhece o índice.Isso pode ser feito em qualquer coisa 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 a resposta do @FlySwat, encontrei esta solução:

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

Você obtém o enumerador usando GetEnumerator e então você faz um loop usando um for laço.No entanto, o truque é tornar a condição do loop listEnumerator.MoveNext() == true.

Desde o MoveNext O método de um enumerador retorna verdadeiro se houver um próximo elemento e ele puder ser acessado, fazendo com que a condição do loop faça o loop parar quando ficarmos sem elementos para iterar.

Usando LINQ, C# 7 e o System.ValueTuple Pacote NuGet, você pode fazer isso:

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

Você pode usar o normal foreach construir e poder acessar o valor e o índice diretamente, não como membro de um objeto, e manter ambos os campos apenas no escopo do loop.Por estas razões, acredito que esta é a melhor solução se você for capaz de usar C# 7 e System.ValueTuple.

Você pode agrupar o enumerador original com outro que contenha as informações do í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();
}

Aqui está o código para o ForEachHelper aula.

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

Não há nada de errado em usar uma variável de contador.Na verdade, quer você use for, foreach while ou do, uma variável de contador deve ser declarada e incrementada em algum lugar.

Portanto, use esta expressão se não tiver certeza se possui uma coleção indexada adequadamente:

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

Caso contrário, use este se você saber que seu indexável coleção é O(1) para acesso ao índice (que será para Array e provavelmente para List<T> (a documentação não diz), mas não necessariamente para outros 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 deverá ser necessário operar “manualmente” o IEnumerator invocando MoveNext() e interrogando Current - foreach está poupando você desse incômodo específico ...se você precisar pular itens, basta usar um continue no corpo do loop.

E apenas para completar, dependendo do que você estava fazendo com o seu índice (as construções acima oferecem bastante flexibilidade), você pode usar o 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));

Nós usamos AsParallel() acima, porque já estamos em 2014 e queremos fazer bom uso desses múltiplos núcleos para acelerar as coisas.Além disso, para LINQ 'sequencial', você só ganha um ForEach() método de extensão ativado List<T> e Array ...e não está claro se usá-lo é melhor do que fazer um simples foreach, já que você ainda está executando thread único para uma sintaxe mais feia.

Aqui está uma solução que acabei de encontrar 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 atualizado

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

Método de extensão:

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

Isso funcionaria para coleções que apoiam IList.

O C# 7 finalmente nos oferece uma maneira elegante de fazer isso:

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ó vai funcionar para uma Lista e não para qualquer IEnumerable, mas no LINQ existe isto:

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 eu não disse que era uma ótima resposta, apenas disse que estava apenas mostrando que era possível fazer o que ele pediu :)

@Graphain Eu não esperaria que fosse rápido - não tenho muita certeza de como funciona, ele poderia repetir toda a lista a cada vez para encontrar um objeto correspondente, o que seria um monte de comparações.

Dito isto, List pode manter um índice de cada objeto junto com a contagem.

Jonathan parece ter uma ideia melhor, se ele pudesse explicar?

Seria melhor apenas manter uma contagem de onde você está no foreach, mais simples e adaptável.

Basta adicionar seu próprio índice.Mantenha simples.

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

É assim que eu faço, o que é bom por sua simplicidade/brevidade, mas se você estiver fazendo muito no corpo do loop obj.Value, vai envelhecer muito 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 que foreach?!

A maneira mais simples é usar para em vez de foreach se você estiver usando Lista .

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

OU se você quiser usar foreach :

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

você pode usar isso para saber o índice de cada Loop:

myList.indexOf(m)

Melhor usar palavra-chave continue construção segura como esta

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

Se a coleção for uma lista, você poderá usar List.IndexOf, como em:

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

A resposta principal afirma:

“Obviamente, o conceito de índice é estranho ao conceito de enumeração e não pode ser feito.”

Embora isso seja verdade para a versão atual do C#, esse não é um limite conceitual.

A criação de um novo recurso de linguagem C# pela MS poderia resolver isso, juntamente com o suporte para uma nova Interface 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 foreach receber um IEnumerable e não puder resolver um IIndexedEnumerable, mas for solicitado com índice var, o compilador C# poderá agrupar a origem com um objeto IndexedEnumerable, que adiciona o código para rastrear o índice.

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

Por que:

  • Foreach parece melhor e em aplicativos de negócios raramente é um gargalo de desempenho
  • Foreach pode ser mais eficiente na memória.Ter um pipeline de funções em vez de converter em novas coleções a cada etapa.Quem se importa se ele usa mais alguns ciclos de CPU, se há menos falhas de cache de CPU e menos GC.Collects
  • Exigir que o codificador adicione código de rastreamento de índice estraga a beleza
  • É muito fácil de implementar (obrigado MS) e é compatível com versões anteriores

Embora a maioria das pessoas aqui não seja MS, esta é uma resposta correta e você pode pressionar a MS para adicionar tal recurso.Você já poderia construir seu próprio iterador com um função de extensão e usar tuplas, mas o MS poderia polvilhar o açúcar sintático para evitar a função de extensão

Minha solução para este problema é um método de extensão WithIndex(),

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

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

Para fins de interesse, Phil Haack acabou de escrever um exemplo disso no contexto de um Razor Templated Delegate (http://haacked.com/archive/2011/04/14/a-better-razor-foreach-loop.aspx)

Efetivamente, ele escreve um método de extensão que envolve a iteração em uma classe "IteratedItem" (veja abaixo), permitindo acesso ao índice e também ao elemento durante a iteração.

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

No entanto, embora isso seja adequado em um ambiente que não seja do Razor, se você estiver executando uma única operação (ou seja,um que poderia ser fornecido como um lambda) não será um substituto sólido da sintaxe for/foreach em contextos que não sejam do Razor.

Não acho que isso deva ser muito eficiente, mas funciona:

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

Eu construí isso em 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.

Você também pode simplesmente usar string.join:

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

Você pode escrever seu loop assim:

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

Depois de adicionar a seguinte estrutura e método de extensão.

A estrutura e o método de extensão encapsulam a funcionalidade 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);
    }
}

Não acredito que exista uma maneira de obter o valor da iteração atual de um loop foreach.Contar-se, parece ser o melhor caminho.

Posso perguntar por que você gostaria de saber?

Parece que você provavelmente estaria fazendo uma das três coisas:

1) Pegar o objeto da coleção, mas neste caso você já o possui.

2) Contando os objetos para posterior pós-processamento... as coleções possuem uma propriedade Count que você pode usar.

3) Definir uma propriedade no objeto com base em sua ordem no loop... embora você possa facilmente definir isso ao adicionar o objeto à coleção.

A menos que sua coleção possa retornar o índice do objeto através de algum método, a única maneira é usar um contador como no seu exemplo.

Entretanto, ao trabalhar com índices, a única resposta razoável para o problema é usar um loop for.Qualquer outra coisa introduz complexidade de código, sem mencionar a complexidade de tempo e espaço.

Que tal algo como isso?Observe que myDelimitedString pode ser nulo se myEnumerable estiver vazio.

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

Acabei de ter esse problema, mas pensar no problema no meu caso deu a melhor solução, sem relação com a solução esperada.

Poderia ser um caso bastante comum, basicamente, estou lendo de uma lista de origem e criando objetos baseados neles em uma lista de destino, porém, primeiro tenho que verificar se os itens de origem são válidos e quero retornar a linha de qualquer erro.À primeira vista, quero colocar o índice no enumerador do objeto na propriedade Current, no entanto, como estou copiando esses elementos, conheço implicitamente o índice atual do destino atual.Obviamente depende do seu objeto de destino, mas para mim era uma Lista, e muito provavelmente implementará ICollection.

ou seja

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

Nem sempre aplicável, mas com frequência suficiente para valer a pena mencionar, eu acho.

De qualquer forma, a questão é que às vezes já existe uma solução não óbvia na lógica que você tem ...

Eu não tinha certeza do que você estava tentando fazer com as informações do índice com base na pergunta.No entanto, em C#, geralmente você pode adaptar o método IEnumerable.Select para obter o índice do que quiser.Por exemplo, posso usar algo assim para saber se um valor é ímpar ou par.

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

Isso lhe daria um dicionário pelo nome se o item era ímpar (1) ou par (0) na lista.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top