Por que não há método de extensão ForEach no IEnumerable?


389

Inspirado por outra pergunta sobre a Zipfunção ausente :

Por que não há ForEachmétodo de extensão na Enumerableclasse? Ou em qualquer lugar? A única classe que obtém um ForEachmétodo é List<>. Existe uma razão pela qual está faltando (desempenho)?


Provavelmente, como sugerido em um post anterior, em que foreach é suportado como uma palavra-chave do idioma em C # e VB.NET (e muitos outros) A maioria das pessoas (inclusive eu) simplesmente escreve uma como é necessário e apropriado.
chrisb

2
Questão relacionada aqui com link para 'resposta oficial' stackoverflow.com/questions/858978/...
Benjol


Eu acho que um ponto muito importante é que um loop foreach é mais fácil de detectar. Se você usa .ForEach (...), é fácil perder esse, quando você olha o código. Isso se torna importante quando você tem problemas de desempenho. É claro que .ToList () e .ToArray () têm o mesmo problema, mas são usados ​​um pouco diferente. Outro motivo pode ser o fato de ser mais comum em um .ForEach modificar a lista de fontes (removendo / adicionando elementos), para que não seja mais uma consulta "pura".
Daniel Bişar

Quando a depuração do código paralelizado, muitas vezes eu tente trocar Parallel.ForEachpara Enumerable.ForEachapenas para redescobrir este último não existe. O C # perdeu um truque para facilitar as coisas aqui.
Coronel Panic

Respostas:


198

Já existe uma declaração foreach incluída no idioma que faz o trabalho a maior parte do tempo.

Eu odiaria ver o seguinte:

list.ForEach( item =>
{
    item.DoSomething();
} );

Ao invés de:

foreach(Item item in list)
{
     item.DoSomething();
}

O último é mais claro e fácil de ler na maioria das situações , embora talvez um pouco mais longo para digitar.

No entanto, devo admitir que mudei de posição sobre esse assunto; um método de extensão ForEach () seria realmente útil em algumas situações.

Aqui estão as principais diferenças entre a declaração e o método:

  • Verificação de tipo: foreach é feito em tempo de execução, ForEach () está em tempo de compilação (Big Plus!)
  • A sintaxe para chamar um delegado é realmente muito mais simples: objects.ForEach (DoSomething);
  • ForEach () pode ser encadeado: embora a maldade / utilidade de tal recurso esteja aberta à discussão.

Esses são todos os pontos importantes apresentados por muitas pessoas aqui e posso ver por que as pessoas estão perdendo a função. Eu não me importaria de a Microsoft adicionar um método ForEach padrão na próxima iteração de estrutura.


39
A discussão aqui fornece a resposta: forums.microsoft.com/MSDN/… Basicamente, foi tomada a decisão de manter os métodos de extensão funcionalmente "puros". Um ForEach incentivaria efeitos colaterais ao usar os métodos de extensão, que não eram a intenção.
mancaus 12/10/08

17
'foreach' pode parecer mais claro se você tiver um background C, mas a iteração interna indica mais claramente sua intenção. Isso significa que você pode fazer coisas como 'sequence.AsParallel (). ForEach (...)'.
Jay Bazuzi

10
Não sei se o seu ponto sobre a verificação de tipo está correto. foreaché o tipo verificado no tempo de compilação se o enumerável estiver parametrizado. Falta apenas a verificação do tipo de tempo de compilação para coleções não genéricas, assim como um ForEachmétodo de extensão hipotético .
Richard Poole

49
Método de extensão seria muito útil em LINQ cadeia com a notação de ponto, como: col.Where(...).OrderBy(...).ForEach(...). Sintaxe muito mais simples, sem poluição de tela com variáveis ​​locais redundantes ou colchetes longos em foreach ().
Jacek Gorgoń

22
"Eu odiaria ver o seguinte" - A questão não era sobre suas preferências pessoais, e você tornou o código mais odioso adicionando feeds de linha estranhos e um par de chaves estranhos. Não é tão odioso list.ForEach(item => item.DoSomething()). E quem disse que é uma lista? O caso de uso óbvio está no final de uma cadeia; com a declaração foreach, você teria que colocar toda a cadeia depois ine a operação a ser executada dentro do bloco, o que é muito menos natural ou consistente.
Jim Balter

73

O método ForEach foi adicionado antes do LINQ. Se você adicionar a extensão ForEach, ela nunca será chamada para instâncias da Lista devido às restrições dos métodos de extensão. Eu acho que a razão de não ter sido adicionado é não interferir com o existente.

No entanto, se você realmente sentir falta dessa pequena função, poderá lançar sua própria versão

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

11
Os métodos de extensão permitem isso. Se já existe uma implementação de instância de um determinado nome de método, esse método é usado em vez do método de extensão. Portanto, você pode implementar um método de extensão ForEach e não colidir com o método List <>. ForEach.
Cameron MacFarland

3
Cameron, acredite, eu sei como os métodos de extensão funcionam :) A lista herda IEnumerable, por isso será algo não óbvio.
aku

9
No Guia de programação de métodos de extensão ( msdn.microsoft.com/en-us/library/bb383977.aspx ): nunca será chamado um método de extensão com o mesmo nome e assinatura que um método de interface ou classe. Parece muito óbvio para mim.
Cameron MacFarland

3
Estou falando de algo diferente? Eu acho que não é bom quando muitas classes têm o método ForEach, mas algumas podem funcionar de maneira diferente.
aku

5
Uma coisa interessante a ser observada sobre List <>. ForEach e Array.ForEach é que eles não estão realmente usando a construção foreach internamente. Ambos usam um loop for simples. Talvez seja uma questão de desempenho ( diditwith.net/2006/10/05/PerformanceOfForeachVsListForEach.aspx ). Mas, considerando que Array e List implementam os métodos ForEach, é surpreendente que eles não tenham implementado pelo menos um método de extensão para IList <>, se não para IEnumerable <> também.
precisa

53

Você pode escrever este método de extensão:

// Possibly call this "Do"
IEnumerable<T> Apply<T> (this IEnumerable<T> source, Action<T> action)
{
    foreach (var e in source)
    {
        action(e);
        yield return e;
    }
}

Prós

Permite encadeamento:

MySequence
    .Apply(...)
    .Apply(...)
    .Apply(...);

Contras

Na verdade, não fará nada até você fazer algo para forçar a iteração. Por esse motivo, não deve ser chamado .ForEach(). Você poderia escrever .ToList()no final ou também este método de extensão:

// possibly call this "Realize"
IEnumerable<T> Done<T> (this IEnumerable<T> source)
{
    foreach (var e in source)
    {
        // do nothing
        ;
    }

    return source;
}

Isso pode ser um desvio muito significativo das bibliotecas C # de remessa; leitores que não estão familiarizados com seus métodos de extensão não saberão o que fazer com seu código.


11
Sua primeira função poderia ser usada sem o método 'realize', se escrito como:{foreach (var e in source) {action(e);} return source;}
jjnguy

3
Você não poderia simplesmente usar o Select em vez do método Apply? Parece-me que aplicar uma ação a cada elemento e produzi-la é exatamente o que o Select faz. Por exemplonumbers.Select(n => n*2);
rasmusvhansen

11
Acho que o Apply é mais legível do que o uso do Select para esse fim. Ao ler uma instrução Select, eu a li como "obtenha algo de dentro" onde, como Apply, é "faça algum trabalho".
Dr. Rob Lang

2
@rasmusvhansen Na verdade, Select()diz aplicar uma função a cada elemento e retornar o valor da função onde Apply()diz aplicar um Actiona cada elemento e retornar o elemento original .
NetMage 18/01/19

11
Isso é equivalente aSelect(e => { action(e); return e; })
Jim Balter 10/10

35

A discussão aqui dá a resposta:

Na verdade, a discussão específica que testemunhei dependia de fato da pureza funcional. Em uma expressão, há frequentemente suposições sobre não ter efeitos colaterais. Ter o ForEach é especificamente convidar efeitos colaterais ao invés de apenas tolerá-los. - Keith Farmer (Parceiro)

Basicamente, foi tomada a decisão de manter os métodos de extensão funcionalmente "puros". Um ForEach incentivaria efeitos colaterais ao usar os métodos de extensão Enumerable, que não eram a intenção.


6
Veja também blogs.msdn.com/b/ericlippert/archive/2009/05/18/… . Fazer isso viola os princípios de programação funcional nos quais todos os outros operadores de sequência se baseiam. É evidente que o único propósito de uma chamada para este método é de causar efeitos colaterais
Michael Freidgeim

7
Esse raciocínio é completamente falso. O caso de uso é que efeitos colaterais são necessários ... sem o ForEach, você precisa escrever um loop foreach. A existência do ForEach não incentiva os efeitos colaterais e sua ausência não os reduz.
Jim Balter

Dado o quão simples é escrever o método de extensão, não há literalmente nenhuma razão para que não seja um padrão de linguagem.
Capitão Prinny

17

Embora eu concorde que é melhor usar a construção foreachinterna na maioria dos casos, acho que o uso dessa variação na extensão ForEach <> é um pouco melhor do que ter que gerenciar o índice regularmente foreach:

public static int ForEach<T>(this IEnumerable<T> list, Action<int, T> action)
{
    if (action == null) throw new ArgumentNullException("action");

    var index = 0;

    foreach (var elem in list)
        action(index++, elem);

    return index;
}
Exemplo
var people = new[] { "Moe", "Curly", "Larry" };
people.ForEach((i, p) => Console.WriteLine("Person #{0} is {1}", i, p));

Daria a você:

Person #0 is Moe
Person #1 is Curly
Person #2 is Larry

Ótima extensão, mas você poderia adicionar alguma documentação? Qual é o uso pretendido do valor retornado? Por que o índice var e não especificamente um int? Uso de amostra?
Mark T

Marca: O valor de retorno é o número de elementos na lista. O índice é digitado especificamente como int, graças à digitação implícita.
21720 Chris Zwiryk

@ Chris, com base no seu código acima, o valor de retorno é o índice com base em zero do último valor na lista, não o número de elementos na lista.
11609 Eric Smith #

@ Chris, não, não: o índice é incrementado após o valor ter sido obtido (incremento do postfix), portanto, após o processamento do primeiro elemento, o índice será 1 e assim por diante. Portanto, o valor de retorno é realmente a contagem de elementos.
27310 MartinStettner

11
@MartinStettner o comentário de Eric Smith em 16/5 foi de uma versão anterior. Ele corrigiu o código em 18/5 para retornar o valor correto.
Michael Blackburn

16

Uma solução alternativa é escrever .ToList().ForEach(x => ...).

profissionais

Fácil de entender - o leitor precisa apenas saber o que é fornecido com o C #, não quaisquer métodos de extensão adicionais.

O ruído sintático é muito leve (apenas adiciona um pouco de código extranioso).

Geralmente, não custa memória extra, pois um nativo .ForEach()precisaria realizar toda a coleção.

contras

A ordem das operações não é ideal. Prefiro perceber um elemento, depois agir sobre ele e depois repetir. Esse código realiza todos os elementos primeiro e depois age sobre eles em sequência.

Se a realização da lista gera uma exceção, você nunca age em um único elemento.

Se a enumeração for infinita (como os números naturais), você estará sem sorte.


11
a) Esta não é nem remotamente uma resposta para a pergunta. b) Essa resposta seria mais clara se mencionasse de antemão que, embora não haja Enumerable.ForEach<T>método, existe um List<T>.ForEachmétodo. c) "Geralmente não custa memória extra, pois um .ForEach () nativo precisaria realizar toda a coleção, de qualquer maneira." - absurdo completo e absoluto. Aqui é a implementação de Enumerable.ForEach <T> que C # daria se o fizesse:public static void ForEach<T>(this IEnumerable<T> list, Action<T> action) { foreach (T item in list) action(item); }
Jim Balter

13

Eu sempre me perguntei isso, é por isso que sempre carrego isso comigo:

public static void ForEach<T>(this IEnumerable<T> col, Action<T> action)
{
    if (action == null)
    {
        throw new ArgumentNullException("action");
    }
    foreach (var item in col)
    {
        action(item);
    }
}

Bom pequeno método de extensão.


2
col pode ser nulo ... você pode verificar isso também.
Amy B

Sim, eu verifico na minha biblioteca, perdi quando fiz isso rapidamente lá.
Aaron Powell

Acho interessante que todos verifiquem o parâmetro action como nulo, mas nunca o parâmetro source / col.
Cameron MacFarland

2
Acho interessante que os parâmetros de todos cheques para null, quando não verificar resultados em uma exceção bem ...
oɔɯǝɹ

De modo nenhum. Uma exceção Ref nula não informa o nome do parâmetro com falha. E você também pode verificar no circuito para que você pudesse jogar uma específica Null Ref dizendo col [index #] foi nula pela mesma razão
Roger Willcocks

8

Portanto, houve muitos comentários sobre o fato de um método de extensão ForEach não ser apropriado, pois não retorna um valor como os métodos de extensão LINQ. Embora essa seja uma afirmação factual, não é inteiramente verdade.

Todos os métodos de extensão LINQ retornam um valor para que possam ser encadeados:

collection.Where(i => i.Name = "hello").Select(i => i.FullName);

No entanto, apenas porque o LINQ é implementado usando métodos de extensão não significa que os métodos de extensão devem ser usados ​​da mesma maneira e retornar um valor. Escrever um método de extensão para expor funcionalidades comuns que não retornam um valor é um uso perfeitamente válido.

O argumento específico sobre o ForEach é que, com base nas restrições dos métodos de extensão (ou seja, que um método de extensão nunca substituirá um método herdado com a mesma assinatura ), pode haver uma situação em que o método de extensão personalizado esteja disponível em todas as classes que impelem IEnumerable <T> exceto Lista <T>. Isso pode causar confusão quando os métodos começam a se comportar de maneira diferente, dependendo se o método de extensão ou o método de herança está sendo chamado ou não.


7

Você pode usar o (encadeado, mas preguiçosamente avaliado) Select, primeiro fazendo sua operação e depois retornando a identidade (ou qualquer outra coisa, se preferir)

IEnumerable<string> people = new List<string>(){"alica", "bob", "john", "pete"};
people.Select(p => { Console.WriteLine(p); return p; });

Você precisará garantir que ele ainda seja avaliado, seja com Count()(a operação mais barata para enumerar afaik) ou com outra operação necessária.

Eu adoraria vê-lo trazido para a biblioteca padrão:

static IEnumerable<T> WithLazySideEffect(this IEnumerable<T> src, Action<T> action) {
  return src.Select(i => { action(i); return i; } );
}

O código acima torna- people.WithLazySideEffect(p => Console.WriteLine(p))se efetivamente equivalente ao foreach, mas preguiçoso e encadeado.


Fantástico, nunca clicou que um select retornado funcionaria assim. Um bom uso da sintaxe do método, pois não acho que você possa fazer isso com a sintaxe da expressão (portanto, nunca pensei nisso dessa maneira antes).
21814 Alex KeySmith

@AlexKeySmith Você poderia fazê-lo com sintaxe de expressão se o C # tivesse um operador de sequência, como seu antepassado. Mas o ponto principal do ForEach é que ele executa uma ação com o tipo void e você não pode usar os tipos void nas expressões em C #.
Jim Balter

imho, agora o Selectnão faz mais o que deveria fazer. é definitivamente uma solução para o que o OP pede - mas sobre a legibilidade do tópico: ninguém esperaria que o select execute uma função.
Matthias Burger

@MatthiasBurger Se Selectnão fizer o que deveria, isso é um problema com a implementação Select, não com o código que chama Select, que não tem influência sobre o que Selectfaz.
Martijn


4

@Coincoin

O poder real do método de extensão foreach envolve a reutilização do Action<>sem adicionar métodos desnecessários ao seu código. Diga que você tem 10 listas e deseja executar a mesma lógica nelas, e uma função correspondente não se encaixa na sua classe e não é reutilizada. Em vez de ter dez para loops ou uma função genérica que obviamente é um auxiliar que não pertence, você pode manter toda a sua lógica em um só lugar (a Action<>. Assim, dezenas de linhas são substituídas por

Action<blah,blah> f = { foo };

List1.ForEach(p => f(p))
List2.ForEach(p => f(p))

etc ...

A lógica está em um só lugar e você não poluiu sua classe.


foreach (var p em List1.Concat (List2) .Concat (List3)) {... fazer coisas ...}
Cameron MacFarland

Se é tão simples como f(p), em seguida, deixar de fora a { }de taquigrafia: for (var p in List1.Concat(List2).Concat(List2)) f(p);.
spoulson

3

A maioria dos métodos de extensão LINQ retorna resultados. O ForEach não se encaixa nesse padrão, pois não retorna nada.


2
O ForEach usa a ação <T>, que é outra forma de delegado
Aaron Powell

2
Exceto pelo direito de leppie, todos os métodos de extensão Enumerable retornam um valor; portanto, o ForEach não se encaixa no padrão. Os delegados não entram nisso.
Cameron MacFarland

Só que um método de extensão não tem que retornar um resultado, ele serve a finalidade de adicionar uma maneira de chamar uma operação freqüentemente usado
Aaron Powell

11
De fato, Slace. E daí que não retorna um valor?
TraumaPony

11
@Martijn iepublic IEnumerable<T> Foreach<T>(this IEnumerable<T> items, Action<T> action) { /* error check */ foreach (var item in items) { action(item); yield return item; } }
McKay

3

Se você tiver F # (que estará na próxima versão do .NET), poderá usar

Seq.iter doSomething myIEnumerable


9
Portanto, a Microsoft optou por não fornecer um método ForEach para um IEnumerable em C # e VB, porque não seria puramente funcional; no entanto, eles o forneceram (nome iter) em sua linguagem mais funcional, F #. A lógica deles me alude.
21817 Scott Hutchinson

3

Parcialmente, é porque os designers da linguagem discordam de uma perspectiva filosófica.

  • Não ter (e testar ...) um recurso é menos trabalhoso do que ter um recurso.
  • Não é realmente mais curto (há alguns casos de função de passagem onde está, mas esse não seria o uso principal).
  • Seu objetivo é ter efeitos colaterais, que não são sobre o que é linq.
  • Por que ter outra maneira de fazer a mesma coisa que um recurso que já temos? (palavra-chave foreach)

https://blogs.msdn.microsoft.com/ericlippert/2009/05/18/foreach-vs-foreach/


2

Você pode usar select quando quiser retornar algo. Caso contrário, você pode usar o ToList primeiro, porque provavelmente não deseja modificar nada na coleção.


a) Esta não é uma resposta para a pergunta. b) A ToList requer tempo e memória extras.
Jim Balter

2

isso já foi implementado? Imagino que não, mas existe algum outro URL que substitua esse ticket? MS connect foi descontinuado
knocte 6/04

Isso não foi implementado. Considere arquivar um novo problema em github.com/dotnet/runtime/issues
Kirill Osenkov em


2

Sou eu ou é a Lista <T> .Foreach praticamente se tornou obsoleta pelo Linq. Originalmente havia

foreach(X x in Y) 

onde Y simplesmente tinha que ser IEnumerable (Pre 2.0) e implementar um GetEnumerator (). Se você olhar para o MSIL gerado, poderá ver que é exatamente o mesmo que

IEnumerator<int> enumerator = list.GetEnumerator();
while (enumerator.MoveNext())
{
    int i = enumerator.Current;

    Console.WriteLine(i);
}

(Consulte http://alski.net/post/0a-for-foreach-forFirst-forLast0a-0a-.aspx para o MSIL)

Então, no DotNet2.0 Generics, apareceu a lista. Foreach sempre me pareceu uma implementação do padrão Vistor (veja Design Patterns de Gamma, Helm, Johnson, Vlissides).

Agora, é claro que na versão 3.5, podemos usar um Lambda para o mesmo efeito; por exemplo, tente http://dotnet-developments.blogs.techtarget.com/2008/09/02/iterators-lambda-and-linq-oh- meu/


11
Eu acredito que o código gerado para "foreach" deve ter um bloco try-finally. Porque o IEnumerator de T herda da interface IDisposable. Portanto, no bloco finalmente gerado, o IEnumerator da instância T deve Disposed.
Morgan Cheng

2

Eu gostaria de expandir a resposta de Aku .

Se você quiser chamar um método com o único objetivo de seu efeito colateral sem iterar todo o enumerável primeiro, poderá usar isso:

private static IEnumerable<T> ForEach<T>(IEnumerable<T> xs, Action<T> f) {
    foreach (var x in xs) {
        f(x); yield return x;
    }
}

11
É o mesmo que:Select(x => { f(x); return x;})
Jim Balter 10/10

0

Ninguém ainda apontou que o ForEach <T> resulta na verificação do tipo de tempo de compilação em que a palavra-chave foreach é verificada em tempo de execução.

Depois de fazer uma refatoração em que os dois métodos foram usados ​​no código, sou a favor do .ForEach, pois tive que procurar falhas de teste / falhas de tempo de execução para encontrar os problemas do foreach.


5
É este realmente o caso? Não vejo como foreachtem menos tempo de compilação. Claro, se sua coleção for um IEnumerable não genérico, a variável de loop será uma object, mas o mesmo seria verdadeiro para um ForEachmétodo de extensão hipotético .
Richard Poole

11
Isso é mencionado na resposta aceita. No entanto, está simplesmente errado (e Richard Poole acima está correto).
Jim Balter
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.