IEnumerable与List - 该使用哪一个?它们如何工作?

854

我对枚举器和 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" 时,它有一些有趣的成员: "inner"、 "outer"、 "innerKeySelector" 和 "outerKeySelector",其中最后两个似乎是委托。 "inner" 成员中没有 "Animal" 实例,而是 "Species" 实例,这对我来说非常奇怪。 "outer" 成员包含 "Animal" 实例。 我想这两个委托决定了哪个进去,哪个出来?

  2. 我发现如果我使用 "Distinct",则 "inner" 包含6个项目(这是不正确的,因为只有2个项目是Distinct),但是 "outer" 包含正确的值。 再次说明,可能是委托方法决定了这一点,但这比我所知道的IEnumerable多一些。

  3. 最重要的是,哪个选项在性能方面更好?

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

还是直接使用枚举器?

如果可以的话,请解释一下或提供一些解释使用IEnumerable的链接。

11个回答

914

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比IEnumerable更快吗?只有在你想要防止查询被执行多次时才是。但总体上哪个更好呢?好吧,在上面的例子中,Leopards和Hyenas都会分别被转换为单个SQL查询,并且数据库仅返回相关的行。但如果我们从AllSpotted()返回了一个List,则可能运行得更慢,因为数据库可能会返回比实际需要的数据更多,我们会浪费时间在客户端进行过滤。
在程序中,将查询延迟转换为列表直到最后可能更好,所以如果我要枚举Leopards和Hyenas超过一次,我会这样做:
List<Animals> Leopards = Feline(AllSpotted()).ToList();
List<Animals> Hyenas = Canine(AllSpotted()).ToList();

12
我认为它们指的是连接操作的两个方面。如果你执行 "SELECT * FROM Animals JOIN Species...",那么连接中的内部部分是Animals,而外部部分则是Species。 - Chris Wenham
12
当我阅读关于**IEnumerable<T> vs IQueryable<T>**的答案时,我看到了类比的解释,即IEnumerable自动强制运行时使用LINQ to Objects来查询集合。所以我对这三种类型感到困惑。https://dev59.com/TnE85IYBdhLWcg3wOw_s - Bronek
9
@Bronek,你链接的答案是正确的。IEnumerable<T>在第一部分之后将成为LINQ-To-Objects,这意味着所有被发现的内容都必须返回才能运行Feline。另一方面,IQueryable<T>将允许查询被细化,仅拉取Spotted Felines。 - Nate
39
这个答案非常误导人!@Nate的评论解释了原因。如果你正在使用IEnumerable<T>,无论如何筛选都将在客户端执行。 - Hans
13
是的,AllSpotted()将会被运行两次。这个答案的更大问题在于以下陈述:“好吧,在上面的代码中,Leopards和Hyenas分别转换为单个SQL查询,并且数据库只返回相关的行。” 这是错误的,因为where子句是在IEnumerable<>上调用的,它只知道如何循环遍历已经来自数据库的对象。如果将AllSpotted()的返回值和Feline()和Canine()的参数改为IQueryable,则过滤将在SQL中发生,这个答案就会有意义了。 - Hans
显示剩余8条评论

315

61
需要指出的是,本文仅针对您代码中公共部分,而非内部运作。ListIList 的实现,因此具有额外的功能,超出了 IList 中的功能(例如 SortFindInsertRange)。如果您强制使用 IList 而不是 List,则会失去这些可能需要的方法。 - Jonathan Twite
14
请不要忘记 IReadOnlyCollection<T> - Dandré
7
在这里加上一个简单的数组 [] 可能会有帮助。 - jbyrd
1
虽然这可能会被人们所不赞同,但感谢您分享这个图形和文章。 - Daniel

158
实现了 IEnumerable 接口的类可以使用 foreach 语法。它基本上有一个方法来获取集合中的下一项。它不需要整个集合在内存中,并且不知道其中有多少项,foreach 只会持续获取下一个项,直到没有为止。
在某些情况下,这非常有用,例如在大型数据库表中,在处理行之前,您不希望将整个表复制到内存中。
现在,List 实现了 IEnumerable,但表示整个集合在内存中。如果您有一个 IEnumerable 并调用 .ToList(),则会创建一个新列表,其中包含内存中枚举的内容。
您的 Linq 表达式返回一个枚举, 默认情况下,当您使用 foreach 迭代时,表达式会执行。 IEnumerable 的 Linq 语句在使用 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 ) ...

4
如果在IEnumerable上执行foreach而没有先将其转换为List,会发生什么?它会将整个集合带入内存吗?还是在迭代foreach循环时逐个实例化元素?谢谢。 - Pap
1
@Pap 后者:它会再次执行,没有任何自动缓存到内存中。 - Keith
1
似乎关键的区别在于:1)整个事物是否在内存中。2)IEnumerable让我使用foreach,而List则会按索引进行。现在,如果我想预先知道thing计数/长度,IEnumerable就不会有帮助,对吧? - Jeb50
1
@MFouadKajj 我不知道你使用的是哪种堆栈,但几乎肯定不会对每一行进行请求。服务器运行查询并计算结果集的起始点,但不会获取整个结果集。对于小的结果集,这可能只需要一次请求,对于大的结果集,您将从结果中发送请求以获取更多行,但它不会重新运行整个查询。 - Keith
1
@shaijut 这取决于具体的提供商,但一般不会。在 Microsoft SQL Server 中,您可以获得一个客户端游标,它保持连接打开,客户端只需请求集合中的下一条记录。这并非没有成本,因为这意味着您需要一个新的连接来并行执行另一个数据库请求或使用MARS连接。这对于评论来说太多了。 - Keith
显示剩余3条评论

122

作为后续,这是因为接口方面还是列表方面?即 IList 也是只读的吗? - Jason Masters
1
IList 不是只读的 - https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.ilist-1?view=netframework-4.7.2 因为 IEnumerable 没有任何添加或删除元素的方法,所以它是只读的。IEnumerable 是 IList 扩展的基本接口之一(请参见链接)。 - CAD bloke
这只是使用方法的问题,而背后可能隐藏着更大的问题 - IEnumerable是只读的,因为它(潜在地)会不断变化。考虑我需要按价值价格升序显示的房屋(假设我有10个)。如果在第二所房子上,我决定更改价格(例如将100万美元添加到价格中) - 整个列表都会更改(顺序现在不同了)。“一个接一个地”和“现在全部”是两回事。 - LongChalk

74
最重要的是要意识到,使用Linq时,查询不会立即被评估。 它只是作为在迭代结果IEnumerable<T>中运行的一部分 - 这就是所有奇怪委托所做的事情。
因此,第一个示例通过调用ToList并将查询结果放入列表中立即评估查询。
第二个示例返回了一个IEnumerable<T>,其中包含运行查询所需的所有信息。
关于性能,答案是“取决于”。 如果您需要立即评估结果(比如说,您稍后要改变正在查询的结构,或者如果您不希望迭代IEnumerable<T>花费很长时间),请使用列表。 否则,请使用IEnumerable<T>。 默认情况应该是使用第二个示例中的按需评估,因为通常使用的内存较少,除非有特定原因存储结果列表。

嗨,感谢您的回答:-)。这几乎解决了我所有的疑问。您有没有想法为什么Enumerable被“分割”成“内部”和“外部”?当我通过鼠标在调试/断点模式下检查元素时会发生这种情况。这可能是Visual Studio的贡献吗?在现场枚举并指示枚举的输入和输出? - Axonn
5
这是Join发挥作用的地方 - 内部和外部是连接的两个方面。通常,不要担心IEnumerables中实际包含了什么内容,因为它与你的实际代码完全不同。只有在迭代输出时才需要真正关注它的实际输出结果 :) - thecoop

50

IEnumerable的优点是延迟执行(通常与数据库一起使用)。查询只有在你实际遍历数据时才会被执行。这是一个等待被需要的查询(也称为惰性加载)。

如果你调用ToList,查询将被执行或者“实例化”(我喜欢这么说)。

两种方法都有其利弊。如果你调用ToList,你可能会消除一些关于查询何时被执行的神秘感。如果你坚持使用IEnumerable,你就能够获得一个优势,即程序直到真正需要时才开始工作。


44

我将分享一个我曾经误用过的概念:

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,仅创建查询而不执行它(在幕后使用表达式树)。这样,您就可以在不触及数据(在此情况下是列表中的数据)的情况下对该查询进行许多操作。List方法获取准备好的查询并针对数据源执行它。 - Bronek
7
实际上,我阅读了所有的答案,而你的是我点赞的答案,因为它清晰地阐述了这两者之间的区别,而没有特别讨论LINQ/SQL。在开始学习LINQ/SQL之前,了解所有这些是至关重要的。佩服。 - BeemerGuy
这是一个重要的区别需要解释,但你的“期望结果”并不是真正的期望。你说得好像这是某种陷阱,而不是设计。 - Neme
1
@Neme,是的,在我了解IEnumerable如何工作之前,这是我的预期,但现在不再是了,因为我知道如何使用它了 ;) - amd
1
虽然这是一个重要的概念需要理解,但它并没有真正回答问题。 - lukkea
请注意,IEnumerable是一条脊椎动物蛇,意味着你可以逐个处理它的"骨头"。你可以通过ToList()方法来"拉直蛇身",但那样它就不再是蛇了,更像是它的快照副本。就像在你的情况下 - IEnumerable可能会持续"进化"和"变化"。 - LongChalk

16
如果你只是想枚举它们,请使用 IEnumerable
但要小心,更改正在枚举的原始集合是一项危险的操作-在这种情况下,您需要先使用ToList。 这将为内存中的每个元素创建一个新列表元素,枚举IEnumerable,因此如果只枚举一次,则性能较差-但更安全,有时List方法很方便(例如在随机访问中)。

2
我不确定说生成一个列表会降低性能是否安全。 - Steven Sudit
1
@ Steven:确实像thecoop和Chris所说的那样,有时使用List可能是必要的。在我的情况下,我已经得出结论它不是必需的。 @ Daren:你所说的“这将为每个元素创建一个新列表在内存中”是什么意思?也许你指的是“列表条目”? ::- )。 - Axonn
1
@Axonn 是的,我是指列表条目。已修正。 - Daren Thomas
@Daren -- 除非你被IEnumerable咬了一口,否则它不会像你期望的那样表现(这也是我在Stack Overflow上研究它的原因:-))。我正在对XPathSelectElements()进行for/each循环,如果没有添加.ToList(),则对.Remove()的子序列调用将无法删除所选的XElements。仍然不确定为什么会出现这种情况--所以继续阅读! - jerhewet
3
修改正在迭代的序列从来都不是一个好主意。会出现不良后果,抽象层次会泄漏,恶魔会闯入我们的维度并制造混乱。所以,.ToList() 在这里很有帮助 ;) - Daren Thomas
显示剩余3条评论

7

IEnumerable 的缺点(延迟执行)是在调用 .ToList() 之前,列表可能会发生更改。以下是一个非常简单的示例:

var persons;
using (MyEntities db = new MyEntities()) {
    persons = db.Persons.ToList(); // It's mine now. In the memory
}
// do what you want with the list of persons;

这将无法起作用。

IEnumerable<Person> persons;
 using (MyEntities db = new MyEntities()) {
     persons = db.Persons; // nothing is brought until you use it;
 }

persons = persons.ToList();  // trying to use it...
// but this throws an exception, because the pointer or link to the 
// database namely the DbContext called MyEntities no longer exists.

7

除了上面发布的所有答案,我再加上我的两分钱。除了List之外,还有许多其他类型也实现了IEnumerable,例如ICollection、ArrayList等等。因此,如果我们在任何方法中将IEnumerable作为参数,我们就可以将任何集合类型传递给该函数。也就是说,我们可以有一个操作抽象而不是具体实现的方法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接