para vs. foreach vs. LINQ

86

Quando escrevo código no Visual Studio, o ReSharper (que Deus o abençoe!) Geralmente sugere que eu mude meu loop for old-school da forma mais compacta para foreach.

E, frequentemente, quando aceito essa alteração, o ReSharper dá um passo à frente e sugere que eu a mude novamente, em um formato LINQ brilhante.

Então, eu me pergunto: existem algumas vantagens reais nessas melhorias? Em uma execução de código bastante simples, não consigo ver nenhum aumento de velocidade (obviamente), mas posso ver o código cada vez menos legível ... Então, eu me pergunto: vale a pena?

beccoblu
fonte
2
Apenas uma observação - a sintaxe LINQ é realmente legível se você estiver familiarizado com a sintaxe SQL. Também existem dois formatos para o LINQ (as expressões lambda do tipo SQL e os métodos encadeados), que podem facilitar a aprendizagem. Poderia ser apenas as sugestões do ReSharper que parecem ilegíveis.
Shauna
3
Como regra geral, normalmente uso o foreach, a menos que trabalhe com uma matriz de comprimento conhecida ou casos semelhantes em que o número de iterações seja relevante. Quanto ao LINQ-ifying, geralmente verei o que o ReSharper faz de um foreach, e se a instrução LINQ resultante for arrumada / trivial / legível, eu a uso e, caso contrário, eu a reverto. Se seria uma tarefa reescrever a lógica original não-LINQ se os requisitos fossem alterados ou se fosse necessário depurar granularmente a lógica da qual a instrução LINQ está abstraindo, eu não o LINQ e a deixo por muito tempo Formato.
Ed Hastings
um erro comum foreaché remover itens de uma coleção enquanto a enumera, onde geralmente foré necessário um loop para iniciar a partir do último elemento.
Slai 7/11
Você pode obter valor de Øredev 2013 - Jessica Kerr - Princípios Funcionais para Desenvolvedores Orientados a Objetos . Linq entra na apresentação logo após a marca de 33 minutos, sob o título "Estilo Declarativo".
Theraot

Respostas:

139

for vs. foreach

Existe uma confusão comum de que essas duas construções são muito semelhantes e que ambas são intercambiáveis ​​assim:

foreach (var c in collection)
{
    DoSomething(c);
}

e:

for (var i = 0; i < collection.Count; i++)
{
    DoSomething(collection[i]);
}

O fato de ambas as palavras-chave começarem pelas mesmas três letras não significa que, semanticamente, elas são semelhantes. Essa confusão é extremamente suscetível a erros, especialmente para iniciantes. Iterar através de uma coleção e fazer algo com os elementos é feito com foreach; fornão precisa e não deve ser usado para esse fim , a menos que você realmente saiba o que está fazendo.

Vamos ver o que há de errado com um exemplo. No final, você encontrará o código completo de um aplicativo de demonstração usado para reunir os resultados.

No exemplo, estamos carregando alguns dados do banco de dados, mais precisamente as cidades da Adventure Works, ordenadas por nome, antes de encontrar "Boston". A seguinte consulta SQL é usada:

select distinct [City] from [Person].[Address] order by [City]

Os dados são carregados pelo ListCities()método que retorna um IEnumerable<string>. Aqui está o que foreachparece:

foreach (var city in Program.ListCities())
{
    Console.Write(city + " ");

    if (city == "Boston")
    {
        break;
    }
}

Vamos reescrevê-lo com a for, assumindo que ambos sejam intercambiáveis:

var cities = Program.ListCities();
for (var i = 0; i < cities.Count(); i++)
{
    var city = cities.ElementAt(i);

    Console.Write(city + " ");

    if (city == "Boston")
    {
        break;
    }
}

Ambos retornam as mesmas cidades, mas há uma enorme diferença.

  • Ao usar foreach, ListCities()é chamado uma vez e produz 47 itens.
  • Ao usar for, ListCities()é chamado 94 vezes e produz 28153 itens no total.

O que aconteceu?

IEnumerableé preguiçoso . Isso significa que ele fará o trabalho somente no momento em que o resultado for necessário. A avaliação preguiçosa é um conceito muito útil, mas possui algumas ressalvas, incluindo o fato de que é fácil perder o (s) momento (s) em que o resultado será necessário, especialmente nos casos em que o resultado é usado várias vezes.

No caso de a foreach, o resultado é solicitado apenas uma vez. No caso de a for implementado no código incorretamente escrito acima , o resultado é solicitado 94 vezes , ou seja, 47 × 2:

  • Toda vez que cities.Count()é chamado (47 vezes),

  • Toda vez que cities.ElementAt(i)é chamado (47 vezes).

Consultar um banco de dados 94 vezes em vez de um é terrível, mas não é a pior coisa que pode acontecer. Imagine, por exemplo, o que aconteceria se a selectconsulta fosse precedida por uma consulta que também insere uma linha na tabela. Certo, teríamos o forque chamará o banco de dados 2.147.483.647 vezes, a menos que esperemos que ele trava antes.

Claro, meu código é tendencioso. Eu deliberadamente usei a preguiça IEnumerablee a escrevi de forma a ligar repetidamente ListCities(). Pode-se notar que um iniciante nunca fará isso, porque:

  • O IEnumerable<T>não tem a propriedade Count, mas apenas o método Count(). Chamar um método é assustador e pode-se esperar que seu resultado não seja armazenado em cache e não seja adequado em um for (; ...; )bloco.

  • A indexação está indisponível IEnumerable<T>e não é óbvio encontrar o ElementAtmétodo de extensão LINQ.

Provavelmente, a maioria dos iniciantes converteria o resultado ListCities()em algo que eles estão familiarizados, como a List<T>.

var cities = Program.ListCities();
var flushedCities = cities.ToList();
for (var i = 0; i < flushedCities.Count; i++)
{
    var city = flushedCities[i];

    Console.Write(city + " ");

    if (city == "Boston")
    {
        break;
    }
}

Ainda assim, esse código é muito diferente da foreachalternativa. Novamente, ele fornece os mesmos resultados e, desta vez, o ListCities()método é chamado apenas uma vez, mas gera 575 itens, enquanto que com foreachele produz apenas 47 itens.

A diferença vem do fato que ToList()faz com que todos os dados a ser carregado a partir do banco de dados. Embora foreachsolicitado apenas nas cidades anteriores a "Boston", o novo forexige que todas as cidades sejam recuperadas e armazenadas na memória. Com 575 strings curtas, provavelmente não faz muita diferença, mas e se estivéssemos recuperando apenas algumas linhas de uma tabela contendo bilhões de registros?

Então, o que é foreachrealmente?

foreachestá mais perto de um loop while. O código que eu usei anteriormente:

foreach (var city in Program.ListCities())
{
    Console.Write(city + " ");

    if (city == "Boston")
    {
        break;
    }
}

pode ser simplesmente substituído por:

using (var enumerator = Program.ListCities().GetEnumerator())
{
    while (enumerator.MoveNext())
    {
        var city = enumerator.Current;
        Console.Write(city + " ");

        if (city == "Boston")
        {
            break;
        }
    }
}

Ambos produzem a mesma IL. Ambos têm o mesmo resultado. Ambos têm os mesmos efeitos colaterais. Obviamente, isso whilepode ser reescrito em um infinito semelhante for, mas seria ainda mais longo e propenso a erros. Você é livre para escolher o que achar mais legível.

Deseja testá-lo você mesmo? Aqui está o código completo:

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Linq;

public class Program
{
    private static int countCalls;

    private static int countYieldReturns;

    public static void Main()
    {
        Program.DisplayStatistics("for", Program.UseFor);
        Program.DisplayStatistics("for with list", Program.UseForWithList);
        Program.DisplayStatistics("while", Program.UseWhile);
        Program.DisplayStatistics("foreach", Program.UseForEach);

        Console.WriteLine("Press any key to continue...");
        Console.ReadKey(true);
    }

    private static void DisplayStatistics(string name, Action action)
    {
        Console.WriteLine("--- " + name + " ---");

        Program.countCalls = 0;
        Program.countYieldReturns = 0;

        var measureTime = Stopwatch.StartNew();
        action();
        measureTime.Stop();

        Console.WriteLine();
        Console.WriteLine();
        Console.WriteLine("The data was called {0} time(s) and yielded {1} item(s) in {2} ms.", Program.countCalls, Program.countYieldReturns, measureTime.ElapsedMilliseconds);
        Console.WriteLine();
    }

    private static void UseFor()
    {
        var cities = Program.ListCities();
        for (var i = 0; i < cities.Count(); i++)
        {
            var city = cities.ElementAt(i);

            Console.Write(city + " ");

            if (city == "Boston")
            {
                break;
            }
        }
    }

    private static void UseForWithList()
    {
        var cities = Program.ListCities();
        var flushedCities = cities.ToList();
        for (var i = 0; i < flushedCities.Count; i++)
        {
            var city = flushedCities[i];

            Console.Write(city + " ");

            if (city == "Boston")
            {
                break;
            }
        }
    }

    private static void UseForEach()
    {
        foreach (var city in Program.ListCities())
        {
            Console.Write(city + " ");

            if (city == "Boston")
            {
                break;
            }
        }
    }

    private static void UseWhile()
    {
        using (var enumerator = Program.ListCities().GetEnumerator())
        {
            while (enumerator.MoveNext())
            {
                var city = enumerator.Current;
                Console.Write(city + " ");

                if (city == "Boston")
                {
                    break;
                }
            }
        }
    }

    private static IEnumerable<string> ListCities()
    {
        Program.countCalls++;
        using (var connection = new SqlConnection("Data Source=mframe;Initial Catalog=AdventureWorks;Integrated Security=True"))
        {
            connection.Open();

            using (var command = new SqlCommand("select distinct [City] from [Person].[Address] order by [City]", connection))
            {
                using (var reader = command.ExecuteReader(CommandBehavior.SingleResult))
                {
                    while (reader.Read())
                    {
                        Program.countYieldReturns++;
                        yield return reader["City"].ToString();
                    }
                }
            }
        }
    }
}

E os resultados:

--- for ---
Abingdon Albany Alexandria Alhambra [...] Bonn Bordeaux Boston

Os dados foram chamados 94 vezes e renderam 28153 itens.

--- para com lista ---
Abingdon Albany Alexandria Alhambra [...] Bonn Bordeaux Boston

Os dados foram chamados 1 vez (es) e renderam 575 itens.

--- --- enquanto
Abingdon Albany Alexandria Alhambra [...] Bonn Bordeaux Boston

Os dados foram chamados 1 vez (es) e renderam 47 item (s).

--- foreach ---
Abingdon Albany Alexandria Alhambra [...] Bonn Bordeaux Boston

Os dados foram chamados 1 vez (es) e renderam 47 item (s).

LINQ vs. maneira tradicional

Quanto ao LINQ, você pode querer aprender programação funcional (FP) - não coisas de C # FP, mas linguagem FP real, como Haskell. Linguagens funcionais têm uma maneira específica de expressar e apresentar o código. Em algumas situações, é superior aos paradigmas não funcionais.

Sabe-se que o FP é muito superior quando se trata de manipular listas ( lista como um termo genérico, não relacionado a List<T>). Dado esse fato, a capacidade de expressar o código C # de uma maneira mais funcional quando se trata de listas é uma coisa bastante boa.

Se você não está convencido, compare a legibilidade do código escrito de maneira funcional e não-funcional na minha resposta anterior sobre o assunto.

Arseni Mourzenko
fonte
1
Pergunta sobre o exemplo de ListCities (). Por que isso funcionaria apenas uma vez? Não tive problemas em prever retornos de produção no passado.
Dante
1
Ele não está dizendo que você obteria apenas um resultado do IEnumerable - ele está dizendo que a consulta SQL (que é a parte mais cara do método) seria executada apenas uma vez - isso é uma coisa boa. Ele leria e produziria todos os resultados da consulta.
happycat
9
@Giorgio: Embora essa questão seja compreensível, ter a semântica de uma linguagem adequada ao que um iniciante pode achar confuso não nos deixaria com uma linguagem muito eficaz.
Steven Evers
4
LINQ não é apenas açúcar semântico. Ele fornece execução atrasada. E no caso de IQueryables (por exemplo, Entity Framework) permite que a consulta seja passada e composta até ser iterada (o que significa que adicionar uma cláusula where a um IQueryable retornado resultará no SQL passado ao servidor na iteração para incluir a cláusula where) descarregando a filtragem no servidor).
Michael Brown
8
Por mais que goste desta resposta, acho que os exemplos são um tanto artificiais. O resumo no final sugere que foreaché mais eficiente do que forquando, na realidade, a disparidade é resultado de um código deliberadamente quebrado. O rigor da resposta se redime, mas é fácil ver como um observador casual pode chegar a conclusões erradas.
Robert Harvey
19

Embora já existam ótimas exposições sobre as diferenças entre for e foreach. Existem algumas deturpações grosseiras do papel do LINQ.

A sintaxe do LINQ não é apenas um açúcar sintático, fornecendo uma aproximação de programação funcional ao C #. O LINQ fornece construções funcionais, incluindo todos os benefícios para C #. Combinado com o retorno de IEnumerable em vez de IList, o LINQ fornece execução adiada da iteração. O que as pessoas normalmente fazem agora é construir e retornar um IList de suas funções como

public IList<Foo> GetListOfFoo()
{
   var retVal=new List<Foo>();
   foreach(var foo in _myPrivateFooList)
   {
      if(foo.DistinguishingValue == check)
      {
         retVal.Add(foo);
      }
   }
   return retVal;
}

Em vez disso, use a sintaxe de retorno de rendimento para criar uma enumeração adiada.

public IEnumerable<Foo> GetEnumerationOfFoo()
{
   //no need to create an extra list
   //var retVal=new List<Foo>();
   foreach(var foo in _myPrivateFooList)
   {
      if(foo.DistinguishingValue == check)
      {
         //yield the match compiler handles the complexity
         yield return foo;
      }
   }
   //no need for returning a list
   //return retVal;
}

Agora a enumeração não ocorrerá até você ToList ou iterar sobre ela. E isso ocorre apenas quando necessário (aqui está uma enumeração de Fibbonaci que não tem um problema de estouro de pilha)

/**
Returns an IEnumerable of fibonacci sequence
**/
public IEnumerable<int> Fibonacci()
{
  int first, second = 1;
  yield return first;
  yield return second;
  //the 46th fibonacci number is the largest that
  //can be represented in 32 bits. 
  for (int i = 3; i < 47; i++)
  {
    int retVal = first+second;
    first=second;
    second=retVal;
    yield return retVal;
  }
}

A realização de um foreach sobre a função Fibonacci retornará a sequência de 46. Se você quiser o 30º, tudo isso será calculado

var thirtiethFib=Fibonacci().Skip(29).Take(1);

Onde nos divertimos muito é o suporte na linguagem para expressões lambda (combinado com as construções IQueryable e IQueryProvider, isso permite a composição funcional de consultas em diversos conjuntos de dados, o IQueryProvider é responsável por interpretar os expressões e criação e execução de uma consulta usando construções nativas da fonte). Não vou entrar nos detalhes detalhados aqui, mas há uma série de postagens no blog mostrando como criar um provedor de consultas SQL aqui

Em resumo, você deve preferir retornar IEnumerable sobre IList quando os consumidores de sua função executarem uma iteração simples. E use os recursos do LINQ para adiar a execução de consultas complexas até que sejam necessárias.

Michael Brown
fonte
13

mas posso ver o código cada vez menos legível

A legibilidade está nos olhos de quem vê. Algumas pessoas podem dizer

var common = list1.Intersect(list2);

é perfeitamente legível; outros podem dizer que isso é opaco e preferem

List<int> common = new List<int>();
for(int i1 = 0; i1 < list1.Count; i1++)
{
    for(int i2 = 0; i2 < list2.Count; i2++)
    {
        if (list1[i1] == list2[i2])
        {
            common.Add(i1);
            break;
        }
    }
}

como deixar mais claro o que está sendo feito. Não podemos dizer o que você acha mais legível. Mas você pode detectar alguns dos meus próprios preconceitos no exemplo que construí aqui ...

AakashM
fonte
28
Honestamente, eu diria que o Linq torna a intenção objetivamente mais legível, enquanto os loops tornam o mecanismo objetivamente mais legível.
jk.
16
Eu corria o mais rápido possível de alguém que me dissesse que a versão for-for-if é mais legível que a versão intersect.
319 Konamiman
3
@ Konamiman - Isso dependeria do que uma pessoa procura quando pensa em "legibilidade". O comentário de jk. ilustra isso perfeitamente. O loop é mais legível no sentido em que você pode ver facilmente como está obtendo seu resultado final, enquanto o LINQ é mais legível no que o resultado final deve ser.
Shauna
2
É por isso que o loop entra na implementação e você usa o Intersect em qualquer lugar.
R. Martinho Fernandes
8
@ Shauna: Imagine a versão for-loop dentro de um método fazendo várias outras coisas; é uma bagunça. Então, naturalmente, você o divide em seu próprio método. Em termos de legibilidade, é o mesmo que IEnumerable <T> .Intersect, mas agora você duplicou a funcionalidade da estrutura e introduziu mais código para manter. A única desculpa é se você precisar de uma implementação personalizada por razões comportamentais, mas estamos falando apenas de legibilidade aqui.
Misko
7

A diferença entre LINQ e foreachrealmente se resume a dois estilos de programação diferentes: imperativo e declarativo.

  • Imperativo: nesse estilo, você diz ao computador "faça isso ... agora faça isso ... agora faça isso agora faça isso". Você alimenta um programa um passo de cada vez.

  • Declarativo: nesse estilo, você diz ao computador qual é o resultado e deixa que ele descubra como chegar lá.

Um exemplo clássico desses dois estilos é comparar o código do assembly (ou C) com o SQL. Na montagem, você fornece instruções (literalmente) uma de cada vez. No SQL, você expressa como unir dados e qual resultado deseja desses dados.

Um bom efeito colateral da programação declarativa é que ela tende a ser um nível um pouco mais alto. Isso permite que a plataforma evolua embaixo de você sem que você precise alterar seu código. Por exemplo:

var foo = bar.Distinct();

O que esta acontecendo aqui? O Distinct está usando um núcleo? Dois? Cinquenta? Nós não sabemos e não nos importamos. Os desenvolvedores .NET podem reescrevê-lo a qualquer momento, desde que continuem a executar o mesmo objetivo, nosso código poderia magicamente ficar mais rápido após uma atualização de código.

Este é o poder da programação funcional. E o motivo pelo qual você encontrará esse código em linguagens como Clojure, F # e C # (escrito com uma mentalidade de programação funcional) é geralmente 3x-10x menor do que seus equivalentes imperativos.

Finalmente, eu gosto do estilo declarativo, porque em C # na maioria das vezes isso me permite escrever código que não altera os dados. No exemplo acima, Distinct()não muda de barra, ele retorna uma nova cópia dos dados. Isso significa que, qualquer que seja a barra, e de onde ela veio, ela não mudou repentinamente.

Assim como os outros pôsteres estão dizendo, aprenda a programação funcional. Isso mudará sua vida. E se você puder, faça-o em uma verdadeira linguagem de programação funcional. Eu prefiro o Clojure, mas F # e Haskell também são excelentes opções.

Timothy Baldridge
fonte
2
O processamento do LINQ é adiado até você realmente iterar sobre ele. var foo = bar.Distinct()é essencialmente um IEnumerator<T>até você ligar .ToList()ou .ToArray(). Essa é uma distinção importante porque, se você não está ciente disso, pode levar a erros de compreensão difíceis.
Berin Loritsch
-5

Outros desenvolvedores da equipe podem ler o LINQ?

Caso contrário, não use ou uma das duas coisas acontecerá:

  1. O seu código não pode ser mantido
  2. Você ficará preso em manter todo o seu código e tudo o que depende dele

Um para cada loop é perfeito para percorrer uma lista, mas se não é isso o que você deve fazer, não use um.

Lhama invertida
fonte
11
hmm, eu aprecio o fato de que, para um único projeto, essa pode ser a resposta, mas, a médio e longo prazo, você deve treinar sua equipe, caso contrário, você terá uma corrida até o final da compreensão do código, o que não parece uma boa ideia.
jk.
21
Na verdade, há uma terceira coisa que poderia acontecer: os outros desenvolvedores poderiam fazer um pequeno esforço e realmente aprender algo novo e útil. Não é algo inédito.
Eric Rei
6
@InvertedLlama se eu estivesse em uma empresa onde os desenvolvedores precisam de treinamento formal para entender novos conceitos de linguagem, então eu estaria pensando em encontrar uma nova empresa.
Wyatt Barnett
13
Talvez você possa se safar dessa atitude com as bibliotecas, mas quando se trata dos principais recursos da linguagem, isso não é suficiente. Você pode escolher estruturas. Mas um bom programador .NET precisa entender todos os recursos da linguagem e da plataforma principal (System. *). E considerando que você não pode nem usar o EF corretamente sem usar o Linq, devo dizer ... hoje em dia, se você é um programador .NET e não conhece o Linq, é incompetente.
Timothy Baldridge
7
Isso já tem votos suficientes, então não vou acrescentar isso, mas um argumento que apóia colegas de trabalho ignorantes / incompetentes nunca é válido.
Steven Evers