ChatGPT解决这个技术问题 Extra ChatGPT

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

我对枚举器的工作方式和 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*/ }

我注意到如果我使用 IEnumerable,当我调试和检查“sel”(在这种情况下是 IEnumerable)时,它有一些有趣的成员:“inner”、“outer”、“innerKeySelector”和“outerKeySelector”,最后两个似乎是代表。 “内部”成员中没有“动物”实例,而是“物种”实例,这对我来说很奇怪。 “外部”成员确实包含“动物”实例。我想这两个代表决定了哪些进,哪些出?我注意到如果我使用“Distinct”,“inner”包含 6 个项目(这是不正确的,因为只有 2 个是 Distinct),但“outer”确实包含正确的值。同样,委托方法可能决定了这一点,但这比我对 IEnumerable 的了解要多一些。最重要的是,这两个选项中哪一个在性能方面是最好的?

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

或者直接使用枚举器?

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


A
Amirhossein Mehrvarzi

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() 返回了一个列表,那么它可能会运行得更慢,因为数据库返回的数据可能比实际需要的多得多,而且我们浪费了在客户端进行过滤的周期。

在程序中,将查询转换为列表可能会更好,直到最后,所以如果我要多次列举 Leopards 和 Hyenas,我会这样做:

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

我认为它们指的是连接的两个方面。如果您执行“SELECT * FROM Animals JOIN Species...”,则连接的内部部分是 Animals,外部部分是 Species。
当我阅读有关以下内容的答案时:IEnumerable<T> vs IQueryable<T> 看到了类比的解释,就是让IEnumerable自动强制运行时使用LINQ to Objects来查询集合。所以我对这三种类型感到困惑。 stackoverflow.com/questions/2876616/…
@Bronek您链接的答案是正确的。 IEnumerable<T> 将是第一部分之后的 LINQ-To-Objects,这意味着必须返回所有发现的对象才能运行 Feline。另一方面,IQuertable<T> 将允许细化查询,仅下拉 Spotted Felines。
这个答案非常具有误导性! @Nate 的评论解释了原因。如果您使用的是 IEnumerable,则过滤器无论如何都会发生在客户端。
是的 AllSpotted() 将运行两次。这个答案的更大问题是下面的陈述:“在上面,Leopards 和 Hyenas 分别被转换为单个 SQL 查询,并且数据库只返回相关的行。”这是错误的,因为在 IEnumerable<> 上调用 where 子句,并且它只知道如何循环遍历已经来自数据库的对象。如果您将 AllSpotted() 以及 Feline() 和 Canine() 的参数返回到 IQueryable 中,那么过滤器将在 SQL 中发生,并且这个答案是有意义的。
r
rubStackOverflow

这里有一篇非常好的文章:Claudio Bernasconi 的技术博客:When to use IEnumerable, ICollection, IList and List

这里有一些关于场景和功能的基础知识点:

https://i.stack.imgur.com/4dKYm.png


应该指出的是,本文仅针对您的代码中面向公众的部分,而不是内部工作。 ListIList 的实现,因此在 IList(例如 SortFindInsertRange)之上具有额外的功能。如果您强迫自己使用 IList 而不是 List,您可能会失去这些您可能需要的方法
不要忘记IReadOnlyCollection<T>
在此处也包含一个普通数组 [] 可能会有所帮助。
虽然可能不受欢迎,但感谢您分享此图形和文章
K
Keith

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

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

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

现在 List 实现了 IEnumerable,但表示内存中的整个集合。如果您有 IEnumerable 并调用 .ToList(),您将创建一个新列表,其中包含内存中的枚举内容。

您的 linq 表达式返回一个枚举,默认情况下,当您使用 foreach 进行迭代时,表达式会执行。迭代 foreach 时会执行 IEnumerable linq 语句,但您可以使用 .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 ) ...

但是,如果在 IEnumerable 上执行 foreach 而不先将其转换为 List 会发生什么?它是否将整个收藏带入了记忆?或者,它是否在迭代 foreach 循环时一一实例化元素?谢谢
@Pap 后者:它再次执行,没有任何东西自动缓存在内存中。
似乎关键差异是 1) 是否在内存中。 2) IEnumerable 让我使用 foreach 而 List 将通过说索引。现在,如果我想事先知道 thingcount/length,IEnumerable 将无济于事,对吧?
@MFouadKajj 我不知道您使用的是什么堆栈,但几乎可以肯定它不会对每一行发出请求。服务器运行查询并计算结果集的起点,但没有得到全部内容。对于小型结果集,这可能是一次旅行,对于大型结果集,您发送的请求是从结果中获取更多行,但它不会重新运行整个查询。
@shaijut 它不应该,但它可能取决于特定的提供者。在 Microsoft SQL Server 中,您会得到一个保持连接打开的 client cursor,并且客户端只请求集合中的下一条记录。这并非没有成本,因为这意味着您需要一个新连接来并行执行另一个数据库请求或一个 MARS 连接。评论实在太多了
C
Community

没有人提到一个关键的区别,讽刺地回答了一个作为重复的问题而结束的问题。

IEnumerable 是只读的,而 List 不是。

请参阅Practical difference between List and IEnumerable


作为后续,这是因为接口方面还是因为列表方面?即 IList 也是只读的吗?
IList 不是只读的 - docs.microsoft.com/en-us/dotnet/api/… IEnumerable 是只读的,因为它在构造后缺少任何添加或删除任何内容的方法,它是 IList 扩展的基本接口之一(参见链接)
这只是一个用法问题,隐藏了一个更大的潜在问题 - IEnumerable 是只读的,因为它(可能)不断变化。考虑我必须按价值升序显示的房子(比如我有 10 个)。如果在第二套房子上,我决定改变价格(比如在价格上增加一百万美元)——整个清单都会改变(现在顺序不同了)。 “一次一个”和“现在全部”是两个不同的东西。
h
h4ck3rm1k3

要意识到的最重要的事情是,使用 Linq,查询不会立即得到评估。它仅作为在 foreach 中遍历生成的 IEnumerable<T> 的一部分运行 - 这就是所有奇怪的代表正在做的事情。

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

就性能而言,答案是视情况而定。如果您需要立即评估结果(例如,您正在改变稍后要查询的结构,或者如果您不希望 IEnumerable<T> 上的迭代花费很长时间),请使用列表。否则使用 IEnumerable<T>。默认应该是使用第二个示例中的按需评估,因为这通常使用较少的内存,除非有特定原因将结果存储在列表中。


您好,感谢您回答 ::- )。这几乎消除了我所有的疑惑。知道为什么 Enumerable 被“拆分”为“内部”和“外部”吗?当我通过鼠标在调试/中断模式下检查元素时会发生这种情况。这可能是 Visual Studio 的贡献吗?现场枚举并指示Enum的输入输出?
这就是 Join 的作用 - 内部和外部是连接的两侧。通常,不必担心 IEnumerables 中的实际内容,因为它与您的实际代码完全不同。仅在迭代时担心实际输出:)
M
Matt Sherman

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

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

两者都有优点和缺点。如果您调用 ToList,您可能会消除关于何时执行查询的一些谜团。如果您坚持使用 IEnumerable,您将获得该程序在实际需要之前不会执行任何工作的优势。


m
maxisam

我将分享我有一天陷入的一个被误用的概念:

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


这是因为 linq 方法(扩展)在这种情况下来自 IEnumerable,其中只创建查询但不执行它(在幕后使用表达式树)。这样,您就可以在不触及数据(在本例中为列表中的数据)的情况下对该查询执行许多操作。 List 方法采用准备好的查询并针对数据源执行它。
实际上,我阅读了所有答案,而您的答案是我投票赞成的答案,因为它清楚地说明了两者之间的区别,而没有专门讨论 LINQ/SQL。在使用 LINQ/SQL 之前了解所有这些是很重要的。钦佩。
这是一个需要解释的重要区别,但您的“预期结果”并不是真正预期的。你说的好像是某种陷阱而不是设计。
@Neme,是的,在我了解 IEnumerable 的工作原理之前,这是我的期望,但现在不是更多,因为我知道如何工作;)
虽然这是一个重要的概念,但理解这并不能真正回答问题。
D
Daren Thomas

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

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


我不确定是否可以肯定地说生成列表意味着性能降低。
@Steven:确实如thecoop 和Chris 所说,有时可能需要使用List。就我而言,我的结论是不是。 @Daren:“这将为内存中的每个元素创建一个新列表”是什么意思?也许您的意思是“列表条目”? ::-)。
@Axonn 是的,我提到了列表条目。固定的。
@Steven 如果您打算迭代 IEnumerable 中的元素,那么首先创建一个列表(并对其进行迭代)意味着您迭代元素 两次。因此,除非您想在列表上执行更高效的操作,否则这确实意味着较低的性能。
@jerhewet:修改被迭代的序列绝不是一个好主意。坏事会发生。抽象会泄漏。恶魔会闯入我们的维度并造成严重破坏。所以是的,.ToList() 在这里有帮助;)
A
Ananth

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


L
LongChalk

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

不同之处在于“列表”对象存储“此时此地”,而“IEnumerable”对象“一次只工作一个”。因此,如果我浏览 ebay 上的所有项目,即使是小型计算机也能处理一次,但“.ToList()”肯定会让我内存不足,无论我的计算机有多大。没有一台计算机可以单独包含和处理如此大量的数据。

[编辑] - 不用说 - 这不是“这个或那个”。通常在同一个类中同时使用列表和 IEnumerable 会很有意义。世界上没有一台计算机可以列出所有素数,因为根据定义,这将需要无限量的内存。但是您可以很容易地想到一个包含 IEnumerable<long> primesclass PrimeContainer,由于显而易见的原因,它还包含一个 SortedList<long> _primes。到目前为止计算的所有素数。下一个要检查的素数只会针对现有素数(直到平方根)运行。这样你就可以同时获得一个素数(IEnumerable)和一个很好的“迄今为止的素数”列表,这是整个(无限)列表的一个很好的近似值。


L
LongChalk

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.