IEnumerable vs List - O que usar? Como eles funcionam?

676

Tenho algumas dúvidas sobre como funcionam os enumeradores e o LINQ. Considere estas duas seleções simples:

List<Animal> sel = (from animal in Animals 
                    join race in Species
                    on animal.SpeciesKey equals race.SpeciesKey
                    select animal).Distinct().ToList();

ou

IEnumerable<Animal> sel = (from animal in Animals 
                           join race in Species
                           on animal.SpeciesKey equals race.SpeciesKey
                           select animal).Distinct();

Alterei os nomes dos meus objetos originais para que isso pareça um exemplo mais genérico. A consulta em si não é tão importante. O que eu quero perguntar é o seguinte:

foreach (Animal animal in sel) { /*do stuff*/ }
  1. Percebi que se eu usar IEnumerable, quando depurar e inspecionar "sel", que nesse caso é o IEnumerable, ele possui alguns membros interessantes: "inner", "outer", "innerKeySelector" e "outerKeySelector", esses dois últimos aparecem para ser delegados. O membro "interno" não possui instâncias "Animal", mas instâncias "Espécies", o que foi muito estranho para mim. O membro "externo" contém instâncias "Animal". Presumo que os dois delegados determinem qual entra e o que sai dela?

  2. Percebi que, se eu usar "Distinto", o "interno" conterá 6 itens (isso está incorreto, pois apenas 2 são Distintos), mas o "externo" conterá os valores corretos. Novamente, provavelmente os métodos delegados determinam isso, mas isso é um pouco mais do que eu sei sobre IEnumerable.

  3. Mais importante, qual das duas opções é a melhor em termos de desempenho?

A conversão da lista do mal via .ToList()?

Ou talvez usando o enumerador diretamente?

Se puder, explique um pouco ou ative alguns links que explicam esse uso do IEnumerable.

Axonn
fonte

Respostas:

738

IEnumerabledescreve o comportamento, enquanto List é uma implementação desse comportamento. Ao usar IEnumerable, você dá ao compilador a chance de adiar o trabalho até mais tarde, possivelmente otimizando ao longo do caminho. Se você usar ToList (), força o compilador a reificar os resultados imediatamente.

Sempre que estou "empilhando" expressões LINQ, eu uso IEnumerable, porque especificando apenas o comportamento, dou ao LINQ a chance de adiar a avaliação e possivelmente otimizar o programa. Lembre-se de como o LINQ não gera o SQL para consultar o banco de dados até que você o enumere? Considere isto:

public IEnumerable<Animals> AllSpotted()
{
    return from a in Zoo.Animals
           where a.coat.HasSpots == true
           select a;
}

public IEnumerable<Animals> Feline(IEnumerable<Animals> sample)
{
    return from a in sample
           where a.race.Family == "Felidae"
           select a;
}

public IEnumerable<Animals> Canine(IEnumerable<Animals> sample)
{
    return from a in sample
           where a.race.Family == "Canidae"
           select a;
}

Agora você tem um método que seleciona uma amostra inicial ("AllSpotted"), além de alguns filtros. Então agora você pode fazer isso:

var Leopards = Feline(AllSpotted());
var Hyenas = Canine(AllSpotted());

Então, é mais rápido usar a lista novamente IEnumerable? Somente se você quiser impedir que uma consulta seja executada mais de uma vez. Mas é melhor no geral? Bem, acima, Leopardos e Hienas são convertidos em consultas SQL únicas cada , e o banco de dados retorna apenas as linhas relevantes. Mas se tivéssemos retornado uma lista de AllSpotted(), ela pode ser executada mais lentamente porque o banco de dados pode retornar muito mais dados do que o necessário, e perdemos ciclos fazendo a filtragem no cliente.

Em um programa, pode ser melhor adiar a conversão de sua consulta em uma lista até o final, portanto, se eu vou enumerar Leopardos e Hienas mais de uma vez, faria o seguinte:

List<Animals> Leopards = Feline(AllSpotted()).ToList();
List<Animals> Hyenas = Canine(AllSpotted()).ToList();
Chris Wenham
fonte
11
Eu acho que eles se referem aos dois lados de uma junção. Se você selecionar "SELECT * FROM Animals JOIN Species ...", a parte interna da junção será Animals e a parte externa será Species.
Chris Wenham
10
Quando li as respostas sobre: IEnumerable <T> vs IQueryable <T> , vi a explicação analógica, de modo que IEnumerable força automaticamente o tempo de execução a usar o LINQ to Objects para consultar a coleção. Então, eu estou confuso entre esses três tipos. stackoverflow.com/questions/2876616/…
Bronek
4
@Bronek A resposta que você vinculou é verdadeira. IEnumerable<T>será LINQ-To-Objects após a primeira parte, o que significa que todas as manchas devem ser retornadas para executar o Feline. Por outro lado, um IQuertable<T>permitirá que a consulta seja refinada, puxando apenas felinos malhados.
Nate
21
Esta resposta é muito enganadora! O comentário de @ Nate explica o porquê. Se você estiver usando IEnumerable <T>, o filtro ocorrerá no lado do cliente, não importa o quê.
Hans Hans
5
Sim AllSpotted () seria executado duas vezes. O maior problema com esta resposta é a seguinte declaração: "Bem, acima, Leopardos e Hienas são convertidos em consultas SQL únicas cada, e o banco de dados retorna apenas as linhas relevantes". Isso é falso, porque a cláusula where está sendo chamada em um IEnumerable <> e só sabe fazer um loop entre objetos que já vêm do banco de dados. Se você retornasse AllSpotted () e os parâmetros Feline () e Canine () para IQueryable, o filtro ocorreria no SQL e essa resposta faria sentido.
Hans
178

Há um artigo muito bom escrito por: TechBlog de Claudio Bernasconi aqui: Quando usar IEnumerable, ICollection, IList e List

Aqui estão alguns pontos básicos sobre cenários e funções:

insira a descrição da imagem aqui insira a descrição da imagem aqui

rubStackOverflow
fonte
25
É importante ressaltar que este artigo é apenas para partes do código que estão voltadas para o público, não para o funcionamento interno. Listé uma implementação da IListe, como tal, tem uma funcionalidade extra na parte superior daqueles no IList(por exemplo Sort, Find, InsertRange). Se você se obriga a usar IListmais List, você perde esses métodos que você pode exigir
Jonathan Twite
4
Não esqueçaIReadOnlyCollection<T>
Dandré
2
Pode ser útil incluir []aqui também uma matriz simples .
Jbyrd 06/12/19
Embora possa ser desaprovado, obrigado por compartilhar este gráfico e artigo
Daniel
133

Uma classe que implementa IEnumerablepermite que você use a foreachsintaxe.

Basicamente, ele possui um método para obter o próximo item da coleção. Ele não precisa que toda a coleção esteja na memória e não sabe quantos itens estão nela, foreachapenas continua recebendo o próximo item até que ele se esgote.

Isso pode ser muito útil em determinadas circunstâncias, por exemplo, em uma tabela massiva de banco de dados, você não deseja copiar tudo na memória antes de começar a processar as linhas.

Agora Listimplementa IEnumerable, mas representa a coleção inteira na memória. Se você possui um IEnumerablee você chama, .ToList()cria uma nova lista com o conteúdo da enumeração na memória.

Sua expressão linq retorna uma enumeração e, por padrão, a expressão é executada quando você itera usando o foreach. Uma IEnumerableinstrução linq é executada quando você itera o foreach, mas você pode forçá-lo a iterar mais cedo usando .ToList().

Aqui está o que eu quero dizer:

var things = 
    from item in BigDatabaseCall()
    where ....
    select item;

// this will iterate through the entire linq statement:
int count = things.Count();

// this will stop after iterating the first one, but will execute the linq again
bool hasAnyRecs = things.Any();

// this will execute the linq statement *again*
foreach( var thing in things ) ...

// this will copy the results to a list in memory
var list = things.ToList()

// this won't iterate through again, the list knows how many items are in it
int count2 = list.Count();

// this won't execute the linq statement - we have it copied to the list
foreach( var thing in list ) ...
Keith
fonte
2
Mas o que acontece se você executar um foreach em um IEnumerable sem convertê-lo em uma lista primeiro ? Traz toda a coleção na memória? Ou instancia o elemento um por um, enquanto itera sobre o loop foreach? graças
Pap
@ Toque no último: ele é executado novamente, nada é automaticamente armazenado em cache na memória.
Keith
Parece que a diferença de chave é 1) coisa toda na memória ou não. 2) IEnumerable, deixe-me usar foreachenquanto a Lista passa por, digamos, índice. Agora, se eu gostaria de saber a contagem / duração de thingantemão, o IEnumerable não ajudará, certo?
Jeb50
@ Jeb50 Não exatamente - ambos Liste Arrayimplementar IEnumerable. Você pode pensar IEnumerableno menor denominador comum que funciona tanto em coleções de memória quanto em grandes que recebem um item de cada vez. Quando você liga, IEnumerable.Count()você pode ligar para uma .Lengthpropriedade rápida ou passar por toda a coleção - o ponto é que IEnumerablevocê não sabe. Isso pode ser um problema, mas se você está fazendo foreachisso, não se importa - seu código funcionará com um Arrayou DataReadero mesmo.
Keith
1
@MFouadKajj Não sei qual pilha você está usando, mas quase certamente não está fazendo uma solicitação com cada linha. O servidor executa a consulta e calcula o ponto inicial do conjunto de resultados, mas não obtém tudo. Para conjuntos de resultados pequenos, é provável que seja uma viagem única; para os grandes, você está enviando uma solicitação para mais linhas a partir dos resultados, mas não executa novamente a consulta inteira.
Keith
97

Ninguém mencionou uma diferença crucial, ironicamente respondida em uma pergunta encerrada como duplicada.

IEnumerable é somente leitura e Lista não.

Consulte Diferença prática entre List e IEnumerable

Cara de CAD
fonte
Como acompanhamento, isso é devido ao aspecto da interface ou ao aspecto da lista? ie IList também é somente leitura?
Jason Masters
O IList não é somente leitura - docs.microsoft.com/en-us/dotnet/api/… IEnumerable é somente leitura porque não possui métodos para adicionar ou remover nada depois de construído, é uma das interfaces base que O IList se estende (veja o link)
CAD bloke
67

O mais importante a ser observado é que, usando o Linq, a consulta não é avaliada imediatamente. Ele é executado apenas como parte da iteração, resultando IEnumerable<T>em um foreach- é o que todos os delegados estranhos estão fazendo.

Portanto, o primeiro exemplo avalia a consulta imediatamente chamando ToListe colocando os resultados da consulta em uma lista.
O segundo exemplo retorna um IEnumerable<T>que contém todas as informações necessárias para executar a consulta posteriormente.

Em termos de desempenho, a resposta é que depende . Se você precisar que os resultados sejam avaliados de uma só vez (por exemplo, você está alterando as estruturas que está consultando mais tarde ou se não quiser que a iteração IEnumerable<T>demore muito tempo) use uma lista. Caso contrário, use um IEnumerable<T>. O padrão deve ser usar a avaliação sob demanda no segundo exemplo, pois geralmente usa menos memória, a menos que haja um motivo específico para armazenar os resultados em uma lista.

thecoop
fonte
Olá e obrigado por responder: -). Isso esclareceu quase todas as minhas dúvidas. Alguma idéia de por que o Enumerável é "dividido" em "interno" e "externo"? Isso acontece quando eu inspeciono o elemento no modo de depuração / interrupção via mouse. Essa é talvez a contribuição do Visual Studio? Enumerando no local e indicando entrada e saída do Enum?
Axonn
5
É Joinisso que faz o trabalho - interno e externo são os dois lados da junção. Geralmente, não se preocupe com o que realmente está no arquivo IEnumerables, pois será completamente diferente do seu código real. Apenas se preocupar com a saída real quando você iterar sobre ele :)
thecoop
40

A vantagem do IEnumerable é a execução adiada (geralmente com bancos de dados). A consulta não será executada até que você efetue um loop através dos dados. É uma consulta aguardando até que seja necessária (também conhecida como carregamento lento).

Se você chamar ToList, a consulta será executada ou "materializada", como eu gostaria de dizer.

Há prós e contras para ambos. Se você chamar ToList, poderá remover algum mistério sobre quando a consulta é executada. Se você seguir o IEnumerable, terá a vantagem de que o programa não realiza nenhum trabalho até que seja realmente necessário.

Matt Sherman
fonte
25

Vou compartilhar um conceito mal usado no qual caí em um dia:

var names = new List<string> {"mercedes", "mazda", "bmw", "fiat", "ferrari"};

var startingWith_M = names.Where(x => x.StartsWith("m"));

var startingWith_F = names.Where(x => x.StartsWith("f"));


// updating existing list
names[0] = "ford";

// Guess what should be printed before continuing
print( startingWith_M.ToList() );
print( startingWith_F.ToList() );

Resultado esperado

// I was expecting    
print( startingWith_M.ToList() ); // mercedes, mazda
print( startingWith_F.ToList() ); // fiat, ferrari

Resultado atual

// what printed actualy   
print( startingWith_M.ToList() ); // mazda
print( startingWith_F.ToList() ); // ford, fiat, ferrari

Explicação

Conforme outras respostas, a avaliação do resultado foi adiada até a chamada ToListou métodos de chamada semelhantes, por exemploToArray .

Então, eu posso reescrever o código neste caso como:

var names = new List<string> {"mercedes", "mazda", "bmw", "fiat", "ferrari"};

// updating existing list
names[0] = "ford";

// before calling ToList directly
var startingWith_M = names.Where(x => x.StartsWith("m"));

var startingWith_F = names.Where(x => x.StartsWith("f"));

print( startingWith_M.ToList() );
print( startingWith_F.ToList() );

Jogar arround

https://repl.it/E8Ki/0

amd
fonte
1
Isso ocorre por causa dos métodos linq (extensão) que, neste caso, vêm do IEnumerable, onde apenas cria uma consulta, mas não a executa (nos bastidores, as árvores de expressão são usadas). Dessa forma, você tem a possibilidade de fazer muitas coisas com essa consulta sem tocar nos dados (nesse caso, dados na lista). O método List pega a consulta preparada e a executa na fonte de dados.
Bronek
2
Na verdade, eu li todas as respostas, e a sua foi a que eu votei, porque indica claramente a diferença entre as duas sem falar especificamente sobre LINQ / SQL. É essencial saber tudo isso ANTES de chegar ao LINQ / SQL. Admirar.
BeemerGuy
Essa é uma diferença importante para explicar, mas o seu "resultado esperado" não é realmente esperado. Você está dizendo como se fosse algum tipo de pegadinha, em vez de design.
NEM22
@Neme, sim Era minha expectativa antes de eu entender como IEnumerablefunciona, mas agora não é mais uma vez eu sei como;)
amd
15

Se tudo o que você deseja fazer é enumerá-los, use o IEnumerable.

Cuidado, porém, que alterar a coleção original que está sendo enumerada é uma operação perigosa - nesse caso, você desejará ToListprimeiro. Isso criará um novo elemento de lista para cada elemento na memória, enumerando IEnumerablee, portanto, terá menos desempenho se você enumerar apenas uma vez - mas mais seguro e, às vezes, os Listmétodos forem úteis (por exemplo, acesso aleatório).

Daren Thomas
fonte
1
Não sei se é seguro dizer que gerar uma lista significa desempenho inferior.
Steven Sudit 02/09/10
@ Steven: na verdade, como thecoop e Chris disseram, às vezes pode ser necessário usar uma lista. No meu caso, concluí que não é. @ Daren: o que você quer dizer com "isso criará uma nova lista para cada elemento na memória"? Talvez você quis dizer uma "entrada na lista"? :: -).
Axonn
@ Axonn sim, eu mentir entrada da lista. fixo.
Daren Thomas
@Steven Se você planeja iterar sobre os elementos no IEnumerable, a criação de uma lista primeiro (e a iteração sobre isso) significa que você iterará os elementos duas vezes . Portanto, a menos que você queira executar operações mais eficientes na lista, isso realmente significa desempenho inferior.
Daren Thomas
3
@jerhewet: nunca é uma boa ideia modificar uma sequência que está sendo repetida. Coisas ruins vão acontecer. Abstrações vazam. Demônios vão invadir nossa dimensão e causar estragos. Então, sim, .ToList()ajuda aqui;)
Daren Thomas
5

Além de todas as respostas postadas acima, aqui estão meus dois centavos. Existem muitos outros tipos além de List que implementam IEnumerable como ICollection, ArrayList etc. Portanto, se tivermos IEnumerable como parâmetro de qualquer método, podemos passar qualquer tipo de coleção para a função. Ou seja, podemos ter um método para operar em abstração e não em nenhuma implementação específica.

Ananth
fonte
1

Existem muitos casos (como uma lista infinita ou uma lista muito grande) em que IEnumerable não pode ser transformado em uma lista. Os exemplos mais óbvios são todos os números primos, todos os usuários do facebook com seus detalhes ou todos os itens no ebay.

A diferença é que os objetos "Lista" são armazenados "aqui e agora", enquanto os objetos "IEnumerable" funcionam "apenas um de cada vez". Portanto, se eu estiver analisando todos os itens no ebay, um de cada vez seria algo que até um computador pequeno pode suportar, mas ".ToList ()" certamente me esgotaria a memória, não importando o tamanho do meu computador. Nenhum computador pode, por si só, conter e manipular uma quantidade tão grande de dados.

[Edit] - Escusado será dizer - não é "isso ou aquilo". geralmente faria sentido usar uma lista e um IEnumerable na mesma classe. Nenhum computador no mundo poderia listar todos os números primos, porque, por definição, isso exigiria uma quantidade infinita de memória. Mas você poderia facilmente pensar em um class PrimeContainerque contém um IEnumerable<long> primes, que por razões óbvias também contém um SortedList<long> _primes. todos os números primos calculados até agora. o próximo primo a ser verificado seria executado apenas contra os primos existentes (até a raiz quadrada). Dessa forma, você obtém os dois primos um de cada vez (IEnumerable) e uma boa lista de "primos até agora", que é uma boa aproximação de toda a lista (infinita).

LongChalk
fonte