Melhorias C # foreach?

9

Eu o encontro frequentemente durante a programação em que quero ter um índice de contagem de loop dentro de um foreach e preciso criar um número inteiro, usá-lo, incrementar etc. Não seria uma boa idéia se houvesse uma palavra-chave introduzida que fosse a contagem de loop dentro de um foreach? Também pode ser usado em outros loops.

Isso vai contra o uso de palavras-chave, visto que o compilador não permitiria que essa palavra-chave fosse usada em outro lugar, exceto em uma construção de loop?

Justin
fonte
7
Você pode fazê-lo com um método de extensão, ver a resposta de Dan Finch: stackoverflow.com/a/521894/866172 (não é a resposta melhor avaliado, mas eu acho que é o mais "limpo")
Jalayn
Perl e coisas como $ _ vêm à mente. Eu odeio essas coisas.
Yam Marcovic
2
Pelo que sei sobre C #, ele tem muitas palavras-chave baseadas em contexto, portanto, isso não "contraria o uso de palavras-chave". Conforme indicado na resposta abaixo, no entanto, você provavelmente só quer usar um for () loop.
Craige
@ Jalayn Por favor, poste isso como resposta. É uma das melhores abordagens.
Apoorv Khurasia
@MonsterTruck: Eu não acho que ele deveria. A solução real seria migrar essa pergunta para o SO e fechá-la como uma duplicata da pergunta à qual Jalayn vincula.
21712 Brian As

Respostas:

54

Se você precisa de uma contagem de loop dentro de um foreachloop, por que não usa apenas um forloop regular ? O foreachloop foi projetado para forsimplificar o uso específico de loops. Parece que você tem uma situação em que a simplicidade foreachnão é mais benéfica.

Ryathal
fonte
11
+1 - bom ponto. É bastante fácil usar IEnumerable.Count ou object.length com um loop for regular para evitar problemas com a identificação do número de elementos no objeto.
11
@ GlenH7: Dependendo da implementação, IEnumerable.Countpode ser caro (ou impossível, se for uma enumeração única). Você precisa saber qual é a fonte subjacente antes de poder fazer isso com segurança.
Preocupação binária
2
-1: Bill Wagner, em C # mais eficaz, descreve por que sempre devemos nos ater ao foreachexcesso for (int i = 0…). Ele escreveu algumas páginas, mas posso resumir em duas palavras - otimização do compilador na iteração. Ok, não foram duas palavras.
Apoorv Khurasia
13
@MonsterTruck: o que Bill Wagner escreveu, já vi situações suficientes como a descrita pelo OP, em que um forloop fornece código melhor legível (e a otimização teórica do compilador geralmente é otimização prematura).
Doc Brown
11
@DocBrown Isso não é tão simples quanto a otimização prematura. Eu mesmo identifiquei gargalos no passado e os corrigi usando essa abordagem (mas admito que estava lidando com milhares de objetos na coleção). Espero publicar um exemplo de trabalho em breve. Enquanto isso, aqui está Jon Skeet . [Ele diz que a melhoria de desempenho não será significativa, mas isso depende muito da coleção e do número de objetos].
Apoorv Khurasia
37

Você pode usar tipos anônimos como este

foreach (var item in items.Select((v, i) => new { Value = v, Index = i }))
{
    Console.WriteLine("Item value is {0} and index is {1}.", item.Value, item.Index);
}

Isso é semelhante à enumeratefunção do Python

for i, v in enumerate(items):
    print v, i
Kris Harper
fonte
+1 Oh ... lâmpada. Eu gosto dessa abordagem. Por que nunca pensei nisso antes?
Phil
17
Quem prefere isso em C # a um loop for clássico claramente tem uma opinião estranha sobre legibilidade. Pode ser um truque interessante, mas eu não permitiria algo assim no código de qualidade da produção. É muito mais barulhento que um loop for clássico e não pode ser entendido tão rápido.
Falcon
4
@ Falcon, esta é uma alternativa válida SE VOCÊ NÃO PODE USAR um loop for fashion à moda antiga (que precisa de uma contagem). Isso é coleções alguns do objeto que eu tinha que resolver ...
Fabricio Araujo
8
@FabricioAraujo: Certamente o C # para loops não exige uma contagem. Tanto quanto sei, eles são iguais aos C's para loops, o que significa que você pode usar qualquer valor booleano para finalizar o loop. Como uma verificação do final da enumeração quando MoveNext retornar false.
Zan Lynx
11
Isso tem o benefício adicional de ter todo o código para lidar com o incremento no início do bloco foreach em vez de precisar encontrá-lo.
JeffO 02/03
13

Estou apenas adicionando a resposta de Dan Finch aqui, conforme solicitado.

Não me dê pontos, dê pontos para Dan Finch :-)

A solução é escrever o seguinte método de extensão genérico:

public static void Each<T>( this IEnumerable<T> ie, Action<T, int> action )
{
    var i = 0;
    foreach ( var e in ie ) action( e, i++ );
}

Que é usado assim:

var strings = new List<string>();
strings.Each( ( str, n ) =>
{
    // hooray
} );
Jalayn
fonte
2
Isso é bem inteligente, mas acho que usar um normal para é mais "legível", especialmente se alguém que acaba mantendo o código pode não ser muito bom em .NET e não está muito familiarizado com o conceito de métodos de extensão . Ainda estou pegando esse pouco de código. :)
MetalMikester
11
Pode-se argumentar em ambos os lados e na pergunta original - eu concordo com a intenção dos pôsteres, há casos em que o foreach lê melhor, mas é necessário um índice, mas sou sempre adverso em adicionar mais açúcar sintático a um idioma, se não for mal necessário - tenho exatamente a mesma solução no meu próprio código com um nome diferente - strings.ApplyIndexed <T> (......)
Mark Mullin
Concordo com Mark Mullin, você pode argumentar em ambos os lados. Eu pensei que era um uso muito inteligente de métodos de extensão e se encaixava na pergunta, então eu a compartilhei.
Jalayn
5

Posso estar errado sobre isso, mas sempre achei que o ponto principal do loop foreach era simplificar a iteração dos dias STL do C ++, ou seja,

for(std::stltype<typename>::iterator iter = stl_type_instance.begin(); iter != stl_type_instance.end(); iter++)
{
   //dereference iter to use each object.
}

compare isso com o uso do .NET de IEnumerable no foreach

foreach(TypeName instance in DescendentOfIEnumerable)
{
   //use instance to use the object.
}

o último é muito mais simples e evita muitas das antigas armadilhas. Além disso, eles parecem seguir regras quase idênticas. Por exemplo, você não pode alterar o conteúdo IEnumerable enquanto estiver dentro do loop (o que faz muito mais sentido se você pensar na maneira STL). No entanto, o loop for geralmente tem um propósito lógico diferente da iteração.

Jonathan Henson
fonte
4
+1: poucas pessoas lembram que o foreach é basicamente açúcar sintático sobre o padrão do iterador.
Steven Evers
11
Bem, existem algumas diferenças; manipulação ponteiro onde presente em .NET está escondido muito profundamente a partir do programador, mas você poderia escrever um loop que se parecia muito com o C ++ exemplo:for(IEnumerator e=collection.GetEnumerator;e.MoveNext();) { ... }
Keith
@KeithS, não se você perceber que tudo o que toca no .NET que não é do tipo estruturado É UM POINTER, apenas com sintaxe diferente (. Em vez de ->). De fato, passar um tipo de referência para um método é o mesmo que passá-lo como ponteiro e passá-lo por referência é passá-lo como ponteiro duplo. Mesma coisa. Pelo menos, é assim que uma pessoa que é o idioma nativo é C ++ pensa sobre isso. Assim, você aprende espanhol na escola pensando em como o idioma está sintaticamente relacionado ao seu próprio idioma nativo.
Jonathan Henson
* tipo de valor não estruturado. desculpa.
Jonathan Henson
2

Se a coleção em questão for uma IList<T>, você pode simplesmente usar forcom a indexação:

for (int i = 0; i < collection.Count; i++)
{
    var item = collection[i];
    // …
}

Caso contrário, você pode usar Select(), ele possui uma sobrecarga que fornece o índice.

Se isso também não é adequado, acho que manter esse índice manualmente é bastante simples, são apenas duas linhas muito curtas de código. Criar uma palavra-chave especificamente para isso seria um exagero.

int i = 0;
foreach (var item in collection)
{
    // …
    i++;
}
svick
fonte
+1, também, se você estiver usando um índice exatamente uma vez dentro do loop, poderá fazer algo como foo(++i)ou foo(i++)dependendo de qual índice é necessário.
2

Se tudo o que você sabe é que a coleção é um IEnumerable, mas precisa acompanhar quantos elementos você processou até o momento (e, portanto, o total quando terminar), você pode adicionar algumas linhas a um loop for básico:

var coll = GetMyCollectionAsAnIEnumerable();
var idx = 0;
for(var e = coll.GetEnumerator(); e.MoveNext(); idx++)
{
   var elem = e.Current;

   //use elem and idx as you please
}

Você também pode adicionar uma variável de índice incrementada a um foreach:

var i=0;
foreach(var elem in coll)
{
   //do your thing, then...
   i++;
}

Se você deseja que isso pareça mais elegante, você pode definir um método de extensão ou dois para "ocultar" esses detalhes:

public static void ForEach<T>(this IEnumerable<T> input, Action<T> action)
{
    foreach(T elem in input)
        action(elem);
}

public static void ForEach<T>(this IEnumerable<T> input, Action<T, int> action)
{
    var idx = 0;
    foreach(T elem in input)
        action(elem, idx++); //post-increment happens after parameter-passing
}

//usage of the index-supporting method
coll.ForEach((e, i)=>Console.WriteLine("Element " + (i+1) + ": " + e.ToString()));
KeithS
fonte
1

O escopo também funciona se você deseja manter o bloco limpo de colisões com outros nomes ...

{ int index = 0; foreach(var el in List) { Console.WriteLine(el + " @ " + index++); } }
Jay
fonte
-1

Para isso eu uso um whileloop. Os loops fortambém funcionam, e muitas pessoas parecem preferi-los, mas eu só gosto de usar foralgo que terá um número fixo de iterações. IEnumerables podem ser gerados em tempo de execução, portanto, na minha opinião, whilefaz mais sentido. Chame de orientação informal de codificação, se quiser.

Robotman
fonte