C#中的IEnumerable、ICollection、IList或List?

6

有人能解释一下为什么我们在C#中经常使用IEnumerable,而不是像我想的那样应该使用List、IList或ICollection吗?

看看这个提交修复。 enter image description here

好的,第一点)IEnumerable只应用于只读集合!

太好了,每个数据库调用都需要在原始线程处理完上下文后加上ToList(),以防止多线程崩溃。大家都在反复强调(我知道),那么为什么我们需要不断地在每个模型上调用ToList()来避免混淆、线程安全问题和重复调用呢?

显然,一旦查询被枚举到List中,我们应该从那里开始使用List,以避免混淆、线程安全问题和不断地在每个模型上调用ToList()。

此外,在解析JSON的WebAPI上,已经枚举对象的文本表示,我们再次将其存储为IEnumarable。我们已经知道大小,不需要懒加载它,我们可能会在某个地方修改该对象,那么为什么不将其存储为List。
--编辑
关于这个问题的一个重要方面是,我从来不知道协变和逆变
您不能使用List<IItem>,因为List实现会出现运行时问题,所以必须使用IEnumarable<IItem>——只有在两个不同系统上具有相同实体并需要在一个代码片段上工作的特定情况下才有意义。否则,自提出问题以来,我一直在使用List<>,在业务层中使用它通常正确率达90%。数据库/存储库层现在使用IColletion或IQueryable。

1
为什么程序员会写出有bug的代码呢?嗯,将线程的典型理解乘以IDisposable再乘以async,你就会得到成功的可能性非常低。添加async关键字是一种双刃剑。对于那些使用api的程序员来说,比如那些想编写WinRT代码而不至于死亡的人,它是一个福音;但对于那些创建api的程序员来说,它往往是一个相当粗糙的工具。每个试图让线程看起来容易的抽象都会增加五种难以调试的新故障模式。 - Hans Passant
嗯..所以这意味着由于这个原因,基本规则需要改变 :D - Piotr Kula
2个回答

12

很难回答这个问题,因为很容易发表意见而不是答案。但我会尽力。

这是一个关于责任的基本问题,更具体地说,是关于良好API设计的问题。

以下是我对此涉及到的API设计领域的一些思考:

  • 尽可能具体地定义输出类型
  • 尽可能开放输入类型

我来举几个例子。如果你的方法可以接受任何集合,请使用IEnumerable<T>。如果你的方法需要一个可索引的集合,请使用IList<T>或类似类型。如果方法的第一件事就是通过.ToList()将输入转换为列表,请公开说明。如果我已经在外部有一个列表,并想要把它传递给你的方法,请使用List<T>

但是,我必须意识到一旦你声明接受IList<T>你就能修改该列表。如果可以接受这点,那么很好,我可以简单地把我的列表发送给你。否则,我将不得不在外部执行.ToList()。这并不比以前更糟糕,只是一个关于谁负责执行此操作的问题。

相反地,如果你的方法返回一个List<T>对象,请返回它作为List<T>IList<T>。不要隐藏在IEnumerable<T>后面。

在这里,您还应该意识到任何收到这个列表的调用者都能修改它。如果不能接受这点,请不要返回IList<T>

尽管如此,在最后一个例子中,您可以说如果那个列表不应该在外部被修改(它不是调用者的列表),则应将其隐藏在IEnumerable<T>后面,但这只是隐藏事实的方式,程序员仍然可以将其强制转换回IList<T>并进行修改。如果不能接受这一点,请通过.ToList()返回副本。

然而,所有这些都归结于责任。

如果您的方法返回IEnumerable<T>,作为使用该方法编写代码的人,我应该依赖于您的方法正确工作。如果您的方法无法工作,因为您已经处置了数据库上下文,那么这是您的问题,您的错误,您的bug。

同样地,如果我需要的东西可以被索引,但除此之外是一个纯只读集合,我应该设计我的方法以采取适当的集合接口来表示这一点。如果我设计我的方法以采取 IEnumerable<T>,我应该知道我具体地说“我会采取任何惰性生成的集合”。如果我不知道这一点或不想这么说,那么我就不应该采取 IEnumerable<T>
因此,为了回答您特定代码行的根本问题,在此必须在内部调用.ToList(),否则您的方法是错误的、有缺陷的、错误的。 OR,您可以重新设计您的方法,使其保持它的惰性特性,但在准备好之前不要处理数据库上下文。在这方面对您的方法进行简单的更改将使用yield return
using (var context = ... )
{
    foreach (var element in context.Request(...))
        yield return element;
}

通过这样的更改,您仍然将去到数据库的实际成本推迟到调用者实际迭代您返回的集合时,并且您仍然正确地处理上下文(假设调用者正确进行迭代)。在这里,我仍会返回 IEnumerable<T>,以明确表示“我是惰性评估的”。

然而,如果您内部生成的列表未被保存,没有人对其负责,您实际上将这个列表的责任交给了调用者,那么您绝对应该将其作为 List<T> 返回。不要将其隐藏在 IEnumerable<T> 后面。


1
令人惊讶的是,现在我完全理解了。我从未将其视为“责任”驱动(也没有人真正像这样谈论它)。每个人都盲目引用基本定义,而实际上无法证明其正确性。我知道我错了,只是没有人能证明我的理论是错误的。我现在知道为什么ToList被隐藏在IEnumarables后面了 - 或者可能我只是真的很慢,花了我十多年才终于理解这一点。感谢您抽出宝贵的时间向我解释这个问题。非常感激。 - Piotr Kula
不仅仅是方法论问题,如果使用迭代器方法,还可以获得一些真正的性能提升。 - Kieran Devlin
由于您提到了性能... :) 我认为一旦您在内存中有一个枚举列表,访问内存中的列表将始终比不断迭代它快。 (但我对迭代器的机制知之甚少) - Piotr Kula
这就是我们通常做的事情...我们使用Linq从原始IEnumarable创建数据子集。例如,Skip(x)Take(y)或甚至Where(x = y) - 当然,我们并没有明确地说我想要索引10025处的元素,但超过一半或每个人的代码都是,我们想要ID = 10025的元素 - 从技术上讲,IEnumarable必须迭代直到找到它,如果我们想要另一个,则必须重新迭代?当然,我知道所有这些都在纳秒内执行,并且对性能没有真正的生活影响,除非您正在处理分形:D - Piotr Kula
感谢您的编辑。我认为这很有意义,如果我们从存储库层返回List,那么它已经被迭代了,不允许进行惰性加载,列表就是数据,没话说。然后,如果将它作为IEnumerable在所有其他层之间传递以标记集合为只读 - 很好。我会遵循这个做法 - 但是我还是知道数据源是一个List(因为我们没有构建我们的IEnumarable的实现,并且也不打算),所以这仍然会让我烦恼...但至少现在我有了一些具体的观点,可以继续编码了。 - Piotr Kula
显示剩余2条评论

0

IEnumerable并没有明确说明数据集是什么类型的集合,因此您可以进行更通用的编程,而不是针对特定类型具体功能的编程。


太棒了,但为什么我们总是需要使一切都变得通用,如果这样会导致问题的话:D 特别是IEnumarable与IList完全不同,它们并不常见,但IEnumarbale是最小的接口。这有点烦人。 - Piotr Kula
  1. 它具体会引起哪些问题?我知道的问题只有一些特定情况下出现。
  2. 并非所有集合都是列表类型,因此您应该使用更通用的类型,例如 IEnumerable。
- Kieran Devlin

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