IEnumerable vs List-使用什么?它们如何工作?


675

我对Enumerators和LINQ的工作方式有疑问。考虑以下两个简单选择:

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

要么

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

我更改了原始对象的名称,以使其看起来像一个更通用的示例。查询本身不是那么重要。我想问的是:

foreach (Animal animal in sel) { /*do stuff*/ }
  1. 我注意到,如果我使用IEnumerable,当我调试并检查“ sel”(在这种情况下为IEnumerable)时,它具有一些有趣的成员:“ inner”,“ outer”,“ innerKeySelector”和“ outerKeySelector”,最后两个出现成为代表。“内部”成员中没有“动物”实例,而是“物种”实例,这对我来说很奇怪。“外部”成员确实包含“动物”实例。我假设这两个代表确定哪个进出什么?

  2. 我注意到,如果我使用“ Distinct”,则“ inner”包含6个项目(这是不正确的,因为只有2个是Distinct),但是“ outer”确实包含正确的值。同样,可能委托方法确定了这一点,但这比我对IEnumerable的了解还多。

  3. 最重要的是,这两个选项中哪个是性能最佳的?

邪恶列表转换通过.ToList()

还是直接使用枚举器?

如果可以的话,也请稍作解释或抛出一些链接来解释IEnumerable的用法。

Answers:


737

IEnumerable描述行为,而List是该行为的实现。使用时IEnumerable,可以使编译器有机会将工作推迟到以后,可能会一直进行优化。如果使用ToList(),则强制编译器立即对结果进行校验。

每当我“堆叠” LINQ表达式时,我都会使用IEnumerable,因为通过仅指定行为,LINQ就有机会推迟评估并可能优化程序。还记得LINQ如何在枚举之前不生成SQL查询数据库吗?考虑一下:

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

现在,您有了一个选择初始样本(“ AllSpotted”)以及一些过滤器的方法。现在,您可以执行以下操作:

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

那么使用List over更快IEnumerable吗?仅当您要阻止查询多次执行时。但是总体上更好吗?在上面的代码中,Leopards和Hyenas分别转换为单个SQL查询,并且数据库仅返回相关的行。但是,如果我们从中返回了List AllSpotted(),则它的运行速度可能会变慢,因为数据库返回的数据可能远远超过实际需要的数据,并且浪费了在客户端进行过滤的周期。

在程序中,最好将查询转换为列表直到最后,最好,因此,如果我要多次通过Leopards和Hyenas枚举,我可以这样做:

List<Animals> Leopards = Feline(AllSpotted()).ToList();
List<Animals> Hyenas = Canine(AllSpotted()).ToList();

11
我认为他们是指联接的两个方面。如果您执行“ SELECT * FROM Animals JOIN Species ...”,则连接的内部为动物,外部为“ Species”。
克里斯·文纳姆

10
当我阅读有关IEnumerable <T>与IQueryable <T>的答案时,我看到了类似的解释,因此IEnumerable自动强制运行时使用LINQ to Objects来查询集合。所以我对这三种类型感到困惑。stackoverflow.com/questions/2876616/…
布罗尼斯瓦夫

4
@Bronek您链接的答案是正确的。IEnumerable<T>在第一部分之后将是LINQ-To-Objects,这意味着必须将所有发现的对象返回以运行Feline。另一方面,IQuertable<T>将允许优化查询,仅拉下Spotted Felines。
内特

21
这个答案很容易误导!@Nate的评论解释了原因。如果您使用的是IEnumerable <T>,则无论如何都将在客户端进行过滤。
汉斯

5
是的AllSpotted()将运行两次。这个答案的最大问题是以下语句:“以上所述,Leopards和Hyenas分别转换为单个SQL查询,并且数据库仅返回相关的行。” 这是错误的,因为在IEnumerable <>上调用了where子句,并且该子句仅知道如何循环遍历数据库中已有的对象。如果您将AllSpotted()以及Feline()和Canine()的参数返回到IQueryable,则过滤器将在SQL中发生,并且此答案很有意义。
汉斯

177

有一篇非常好的文章由:Claudio Bernasconi的TechBlog撰写:何时使用IEnumerable,ICollection,IList和List

这里是有关方案和功能的一些基本知识:

在此处输入图片说明 在此处输入图片说明


25
应该指出的是,本文仅适用于代码的面向公众的部分,而不适用于内部工作。 List是的实现IList,因此,对那些在顶部额外的功能IList(例如SortFindInsertRange)。如果您强迫自己使用IList过度List,则可以松散可能需要的这些方法
Jonathan Twite

4
不要忘记IReadOnlyCollection<T>
Dandré

2
[]在此处也包含一个普通数组可能会有所帮助。
jbyrd

尽管它可能会让人皱眉,但感谢您分享此图片和文章
Daniel

133

实现的类IEnumerable允许您使用foreach语法。

基本上,它具有一种方法来获取集合中的下一个项目。它不需要整个集合存储在内存中,也不知道其中有多少个项目,foreach只需不断获取下一个项目,直到用完为止。

在某些情况下,这可能非常有用,例如,在海量数据库表中,您不想在开始处理行之前将整个内容复制到内存中。

现在List实现IEnumerable,但表示内存中的整个集合。如果您有一个,IEnumerable并且您打电话给.ToList()您,则会使用内存中枚举的内容创建一个新列表。

您的linq表达式返回一个枚举,默认情况下,当您使用进行迭代时,该表达式会执行foreach。一个IEnumerableLINQ语句执行,当你迭代foreach,但是你可以用它迫使迭代更快.ToList()

这就是我的意思:

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

2
但是,如果在IEnumerable上执行foreach 而不先将其转换为List会发生什么?它会将整个集合带入内存吗?还是在迭代foreach循环时逐个实例化元素?谢谢
Pap

@Pap后者:它再次执行,没有任何内容自动缓存在内存中。
基思

似乎关键的区别是1)整个内存是否存在。2)IEnumerable让我用,foreach而List会说索引。现在,如果我想知道的计数/长度thing事前,IEnumerable的无助,对不对?
Jeb50'9

@ Jeb50不完全是-既ListArray实施IEnumerable。您可以将其IEnumerable视为既适用于内存集合,又适用于一次获取一项的大型内存的最小公分母。打电话时,IEnumerable.Count()您可能正在呼叫快速.Length属性或遍历整个集合-重点是IEnumerable您不知道。那可能是个问题,但是如果您只是去处理foreach它,那么您不在乎-您的代码可以使用ArrayDataReader相同的代码工作。
基思

1
@MFouadKajj我不知道您正在使用什么堆栈,但是几乎可以肯定的是,每一行都不会发出请求。服务器运行查询并计算结果集的起点,但并不能说明全部。对于较小的结果集,这很可能是一次旅行,对于较大的结果集,您是从结果中发送请求以获取更多行,但不会重新运行整个查询。
基思

97

没有人提到一个关键的差异,具有讽刺意味的是,作为一个重复的问题,有人回答了一个封闭的问题。

IEnumerable是只读的,而List不是。

请参阅List和IEnumerable之间的实际区别


作为后续措施,是由于接口方面还是由于列表方面?即IList是否也是只读的?
杰森·马斯特斯

IList不是只读的-docs.microsoft.com/zh-cn/dotnet/api/…IEnumerable是只读的,因为它一旦构造便缺乏任何添加或删除任何东西的方法,它是基本接口之一IList扩展(请参阅链接)
CAD bloke

67

要实现的最重要的事情是,使用Linq,查询不会立即得到评估。它只是在遍历结果的过程IEnumerable<T>中运行的foreach-这就是所有奇怪的代表正在做的事情。

因此,第一个示例通过调用ToList查询结果并将其放在列表中来立即评估查询。
第二个示例返回一个IEnumerable<T>,其中包含稍后运行查询所需的所有信息。

就性能而言,答案取决于它。如果您需要一次评估结果(例如,您要对稍后查询的结构进行变异,或者您不希望迭代IEnumerable<T>花费很长时间),请使用列表。否则使用IEnumerable<T>。默认情况下,应该在第二个示例中使用按需评估,因为通常会使用较少的内存,除非出于特殊原因将结果存储在列表中。


您好,感谢您回答::-)。这几乎消除了我的所有疑问。知道为什么Enumerable被“拆分”为“内部”和“外部”吗?当我通过鼠标以调试/中断模式检查元素时,会发生这种情况。这也许是Visual Studio的贡献吗?当场枚举并指示Enum的输入和输出?
Axonn

5
这就是Join它的工作-内部和外部是连接的两个方面。通常,不必担心实际存在的内容IEnumerables,因为它与您的实际代码完全不同。仅在迭代时担心实际输出:)
thecoop 2010年

40

IEnumerable的优点是延迟执行(通常使用数据库)。在您实际遍历数据之前,查询将不会执行。这是一个查询,直到需要它为止(又称延迟加载)。

如果您调用ToList,查询将被执行,或者像我想说的那样“物化”。

两者都有优点和缺点。如果调用ToList,则可以消除执行查询时的神秘性。如果坚持使用IEnumerable,您将获得一个好处,即该程序在实际需要时才起作用。


25

我将分享一个误入歧途的误解:

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

预期结果

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

实际结果

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

说明

根据其他答案,对结果的评估会推迟到ToList例如调用或类似的调用方法之前进行ToArray

因此,在这种情况下,我可以将代码重写为:

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

玩周围

https://repl.it/E8Ki/0


1
这是因为linq方法(扩展名)在这种情况下来自IEnumerable,其中IEnumerable仅创建查询而不执行(在后台使用表达式树)。这样,您就可以在不触摸数据(在这种情况下为列表中的数据)的情况下对该查询执行许多操作。List方法采用准备好的查询,并针对数据源执行查询。
布洛尼克

2
实际上,我阅读了所有答案,而您的答案是我投票赞成的答案,因为它明确指出了两者之间的区别,而没有专门讨论LINQ / SQL。在使用LINQ / SQL之前,必须了解所有这些知识。佩服。
BeemerGuy

这是一个重要的差异,可以解释,但是您的“预期结果”并不是真正预期的。您是说这是某种陷阱而不是设计。
尼姆

@Neme,是的,在我了解IEnumerable工作原理之前,这是我的期望,但是由于我知道该如何做,所以现在不多了;)
2017年

15

如果您只想枚举它们,请使用IEnumerable

但是请注意,更改要枚举的原始集合是一项危险的操作-在这种情况下,您将需要ToList首先进行操作。这将为内存中的每个元素创建一个新的list元素,枚举IEnumerable,因此如果只枚举一次,则性能会降低-但更安全,有时List方法很方便(例如,在随机访问中)。


1
我不确定是否可以肯定地说生成列表意味着性能降低。
史蒂文·苏迪特

// @史蒂文:确实如thecoop和克里斯所说,有时可能有必要使用列表。就我而言,我得出结论不是。// @达人:“这将为内存中的每个元素创建一个新列表”是什么意思?也许您的意思是“列表条目”?::-)。
Axonn

@Axonn是的,我提到了列表条目。固定。
达伦·托马斯

@Steven如果您打算遍历中的元素IEnumerable,则首先创建一个列表(并对其进行遍历)意味着您遍历元素两次。因此,除非您想执行列表上更有效的操作,否则确实确实意味着性能降低。
达伦·托马斯

3
@jerhewet:修改被迭代的序列永远不是一个好主意。坏事会发生。抽象将泄漏。恶魔将闯入我们的视野并造成破坏。所以是的,.ToList()在这里
有所

5

除了上面发布的所有答案外,这是我的两分钱。除了List以外,还有许多其他类型可以实现IEnumerable,例如ICollection,ArrayList等。因此,如果我们将IEnumerable作为任何方法的参数,则可以将任何集合类型传递给该函数。也就是说,我们可以有一种方法可以对抽象进行操作,而不需要任何特定的实现。


1

在许多情况下(例如,无限列表或非常大的列表),IEnumerable无法转换为列表。最明显的例子是所有素数,所有带详细信息的facebook用户或ebay上的所有商品。

区别在于“列表”对象“在此立即存储”,而“ IEnumerable”对象“一次仅存储”。因此,如果我要遍历ebay上的所有项目,即使是一台小型计算机也可以一次处理一次,但是“ .ToList()”肯定会耗尽我的内存,无论我的计算机有多大。没有计算机本身可以包含和处理如此大量的数据。

[编辑]-不用说-它不是“这个或那个”。通常,在同一类中同时使用列表和IEnumerable是很有意义的。世界上没有计算机可以列出所有素数,因为根据定义,这将需要无限量的内存。但是,您很容易想到class PrimeContainer包含的 IEnumerable<long> primes,出于明显的原因,其中也包含SortedList<long> _primes。到目前为止计算的所有素数。下一个要检查的素数将仅针对现有素数运行(直到平方根)。这样,您既会获得-一次素数(即IEnumerable)又获得了一个很好的“到目前为止素数”列表,这与整个(无限)列表非常相似。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.