【问题标题】:Should I always return IEnumerable<T> instead of IList<T>?我是否应该总是返回 IEnumerable<T> 而不是 IList<T>?
【发布时间】:2009-07-02 05:15:35
【问题描述】:

当我编写我的 DAL 或其他返回一组项目的代码时,我是否应该总是做我的 return 语句:

public IEnumerable<FooBar> GetRecentItems()

public IList<FooBar> GetRecentItems()

目前,在我的代码中,我一直在尝试尽可能多地使用 IEnumerable,但我不确定这是否是最佳做法?这似乎是正确的,因为我返回了最通用的数据类型,同时仍然描述了它的作用,但也许这样做是不正确的。

【问题讨论】:

标签: c# ienumerable


【解决方案1】:

当您需要返回可由调用者修改的集合时,框架设计指南建议使用类Collection,或为只读集合返回ReadOnlyCollection

优先于简单的IList 的原因是IList 不会通知调用者它是否只读。

如果您改为返回 IEnumerable&lt;T&gt;,则调用者执行某些操作可能会有些棘手。此外,您不再会给调用者修改集合的灵活性,这是您可能想要也可能不想要的东西。

请记住,LINQ 包含一些技巧,并且会根据执行它们的类型优化某些调用。因此,例如,如果您执行 Count 并且基础集合是 List 它不会遍历所有元素。

就个人而言,对于 ORM,我可能会坚持使用 Collection&lt;T&gt; 作为我的返回值。

【讨论】:

【解决方案2】:

这实际上取决于您使用该特定界面的原因。

例如,IList&lt;T&gt; 有几个 IEnumerable&lt;T&gt; 中没有的方法:

  • IndexOf(T item)
  • Insert(int index, T item)
  • RemoveAt(int index)

和属性:

  • T this[int index] { get; set; }

如果您以任何方式需要这些方法,请务必返回IList&lt;T&gt;

另外,如果使用 IEnumerable&lt;T&gt; 结果的方法期待 IList&lt;T&gt;,它将使 CLR 无需考虑任何所需的转换,从而优化编译代码。

【讨论】:

  • @Jon FDG 建议使用 Collection 或 ReadOnlyCollection 作为集合类型的返回值,请参阅我的答案。
  • 当您说“它将保存 CLR”时,您并不清楚最后一句话的意思。使用 IEnumerable 与 IList 将如何保存它?你能说清楚一点吗?
  • @CoffeeAddict 这个答案三年后我认为你是对的——最后一部分是模糊的。如果期望 IList 作为参数的方法获得 IEnumerable,则 IEnumerable 必须手动包装在新的 List 或其他 IList 实现者中,并且该工作不会由为您准备的 CLR。相反——一个期望 IEnumerable 获得 IList 的方法,可能 必须进行一些拆箱,但事后看来可能不需要这样做,因为 IList 实现了 IEnumerable
【解决方案3】:

一般来说,您应该要求最通用的并尽可能返回最具体的东西。因此,如果您有一个带有参数的方法,并且您只需要 IEnumerable 中可用的内容,那么这应该是您的参数类型。如果您的方法可以返回 IList 或 IEnumerable,则最好返回 IList。这确保了它可以被最广泛的消费者使用。

在你需要的东西上放宽,在你提供的东西上明确。

【讨论】:

  • 我得出了相反的结论:应该接受特定类型,并返回一般类型。但是,您可能是对的,更广泛适用的方法比更受限制的方法更好。我将不得不考虑更多。
  • 接受通用类型作为输入的理由是,它允许您使用尽可能广泛的输入,以便尽可能多地重用组件。另一方面,由于您已经确切知道手头有什么样的对象,因此掩盖它没有多大意义。
  • 我认为我同意使用更通用的参数,但是您返回不那么通用的参数的理由是什么?
  • 好的,让我换一种方式试试。为什么要丢弃信息?如果您只关心结果是 IEnumerable,那么知道它是 IList 是否会伤害您?不,它没有。在某些情况下,这可能是多余的信息,但对您没有害处。现在为了利益。如果您返回 List 或 IList,我可以立即告诉您该集合已被检索,这是 IEnumerable 无法知道的。这可能是也可能不是有用的信息,但再一次,你为什么要扔掉信息?如果您知道有关某事的额外信息,请将其传递出去。
  • 我不确定这是否正确。许多 LINQ 方法执行惰性求值并返回 IEnumerable。无法保证数据已被枚举。
【解决方案4】:

这取决于...

返回最小派生类型 (IEnumerable) 将为您留出最大的余地来更改底层实现。

返回一个更派生的类型 (IList) 为您的 API 的用户提供对结果的更多操作。

我总是建议返回具有用户需要的所有操作的最小派生类型...所以基本上,您首先必须确定在您定义的 API 的上下文中对结果进行哪些操作是有意义的.

【讨论】:

  • 很好的通用答案,因为它也适用于其他方法。
  • 我知道这个答案很老了......但它似乎与文档相矛盾:“如果 IDictionary 接口和 IList 接口均不符合所需的要求集合,而是从 ICollection 接口派生新的集合类以获得更大的灵活性”这似乎暗示应该首选派生类型更多的类型。 (msdn.microsoft.com/en-us/library/92t2ye13(v=vs.110).aspx)\
【解决方案5】:

需要考虑的一点是,如果您使用延迟执行 LINQ 语句来生成 IEnumerable&lt;T&gt;,则在从方法返回之前调用 .ToList() 意味着您的项目可能会被迭代两次 - 一次是为了创建列表,以及调用者循环、过滤或转换您的返回值时的一次。实际时,我喜欢避免将 LINQ-to-Objects 的结果转换为具体的列表或字典,直到必须这样做。如果我的调用者需要一个 List,这是一个简单的方法调用 - 我不需要为他们做出决定,这使得我的代码在调用者只是执行 foreach 的情况下稍微更有效率。

【讨论】:

  • @Joel Mueller,无论如何我通常都会调用 ToList() 。我通常不喜欢将 IQueryable 暴露给我的其他项目。
  • 我更多地指的是 LINQ-to-Objects,其中 IQueryable 通常不会进入图片。当涉及数据库时, ToList() 变得更加必要,因为否则您可能会在迭代之前关闭连接,这不能很好地工作。但是,如果这不是问题,则很容易将 IQueryable 公开为 IEnumerable,而无需在您想要隐藏 IQueryable 时强制进行额外的迭代。
  • 我不需要为他们做那个决定,这使得我的代码在调用者只是做一个 foreach 的情况下稍微更有效率。 - 你不需要不知道调用者能做什么,如果他迭代两次呢?您的延迟操作将执行两次。
【解决方案6】:

List&lt;T&gt; 为调用代码提供了更多功能,例如修改返回的对象和按索引访问。所以问题归结为:在您的应用程序的特定用例中,您是否想要支持这种用途(可能是通过返回一个新构建的集合!),为了调用者的方便 - 或者您是否想要简单案例的速度,当所有调用者需要遍历集合,您可以安全地返回对真正底层集合的引用,而不必担心这会导致它被错误地更改等?

只有您才能回答这个问题,并且只有充分了解您的调用者想要对返回值做什么,以及性能在这里有多重要(您要复制的集合有多大,这有多大可能)瓶颈等)。

【讨论】:

  • “安全地返回对真正底层集合的引用,而不必担心这会导致它被错误地更改” - 即使您返回 IEnumerable,他们不能简单地将其转换回 List 并改变它?
  • 并非每个 IEnumarable 也是 List。如果返回的对象不是继承自 List 或实现 IList 的类型,这将导致 InvalidCastException。
  • List 的问题是它会将您锁定在特定的实现中,首选 Collection 或 ReadOnlyCollection
【解决方案7】:

当您谈论返回值而不是输入参数时,这并不是那么简单。当它是一个输入参数时,您确切地知道您需要做什么。因此,如果您需要能够遍历集合,则采用 IEnumberable,而如果您需要添加或删除,则采用 IList。

在返回值的情况下,它更难。您的来电者期望什么?如果您返回一个 IEnumerable,那么他将不会先验地知道他可以从中创建一个 IList。但是,如果你返回一个 IList,他就会知道他可以迭代它。因此,您必须考虑调用者将如何处理数据。调用者需要/期望的功能是在决定返回什么时应该控制的功能。

【讨论】:

    【解决方案8】:

    我认为你可以使用任何一个,但每个都有一个用途。基本上ListIEnumerable 但你有 计数功能,添加元素,删除元素

    IEnumerable 对元素计数效率不高

    如果集合是只读的,或者集合的修改由Parent 控制,那么只为Count 返回一个IList 不是一个好主意。

    在 Linq 中,IEnumerable&lt;T&gt; 上有一个 Count() 扩展方法,如果基础类型为 IList,CLR 内部将快捷方式到 .Count,因此性能差异可以忽略不计。

    一般来说,我认为(意见)最好在可能的情况下返回 IEnumerable,如果您需要添加这些方法,则将这些方法添加到父类,否则消费者会在违反原则的 Model 中管理集合,例如manufacturer.Models.Add(model) 违反了德米特法则。当然,这些只是指导方针,并不是硬性规定,但在完全掌握适用性之前,盲目跟随总比不跟随要好。

    public interface IManufacturer 
    {
         IEnumerable<Model> Models {get;}
         void AddModel(Model model);
    }
    

    (注意:如果使用 nNHibernate,您可能需要使用不同的访问器映射到私有 IList。)

    【讨论】:

      【解决方案9】:

      TL;博士; – 总结

      • 如果你开发内部软件,请使用特定类型(如List)作为回报 值和最通用的输入参数类型,即使在集合的情况下也是如此。
      • 如果方法是可再发行库的公共 API 的一部分,请使用 接口而不是具体的集合类型来引入返回值和输入参数。
      • 如果方法返回只读集合,请使用IReadOnlyListIReadOnlyCollection 作为返回值类型来显示。

      More

      【讨论】:

        【解决方案10】:

        正如所有人所说,这取决于, 如果您不想在调用层添加/删除功能,那么我将投票支持 IEnumerable,因为它仅提供设计方面我喜欢的迭代和基本功能。 返回 IList 我的投票总是反对它,但主要是你喜欢什么,不喜欢什么。 在性能方面,我认为它们更相似。

        【讨论】:

          【解决方案11】:

          如果您不计入外部代码,最好返回 IEnumerable,因为稍后您可以更改您的实现(不影响外部代码),例如,对于 yield iterator 逻辑并保存内存资源(顺便说一下非常好的语言功能)。

          但是,如果您需要计数项目,请不要忘记在 IEnumerable 和 IList 之间还有另一层 - ICollection

          【讨论】:

            【解决方案12】:

            我可能有点跑题了,因为到目前为止还没有其他人提出建议,但是您为什么不返回 (I)Collection&lt;T&gt; 呢?

            据我所知,Collection&lt;T&gt; 是优于 List&lt;T&gt; 的首选返回类型,因为它抽象了实现。他们都实现了IEnumerable,但对我来说这听起来有点太低级了。

            【讨论】:

              【解决方案13】:

              我认为你可以使用任何一个,但每个都有一个用途。基本上ListIEnumerable 但你有计数功能,添加元素,删除元素

              IEnumerable 在计算元素或获取集合中的特定元素时效率不高。

              List 是一个非常适合查找特定元素、轻松添加或删除元素的集合。

              通常我会尽可能使用List,因为这给了我更大的灵活性。

              使用 List&lt;FooBar&gt; getRecentItems() 而不是 IList&lt;FooBar&gt; GetRecentItems()

              【讨论】:

                【解决方案14】:

                我认为一般规则是使用更具体的类返回,以避免做不必要的工作并给你的调用者更多的选择。

                也就是说,我认为考虑您正在编写的代码比考虑下一个人将编写的代码更重要(在合理范围内)。这是因为您可以对已经存在的代码做出假设。

                请记住,在接口中从 IEnumerable 向上移动到集合会起作用,从集合向下移动到 IEnumerable 会破坏现有代码。

                如果这些意见似乎都相互矛盾,那是因为这个决定是主观的。

                【讨论】:

                  【解决方案15】:

                  IEnumerable&lt;T&gt; 包含List&lt;T&gt; 内部的一小部分内容,其中包含与IEnumerable&lt;T&gt; 相同的内容,但更多!如果您想要一组较小的功能,您只能使用IEnumerable&lt;T&gt;。如果您打算使用更大、更丰富的功能集,请使用 List&lt;T&gt;

                  比萨解释

                  这里有一个更全面的解释,说明在用 Microsoft C# 等 C 语言实例化对象时,为什么要使用 IEnumerable&lt;T&gt;List&lt;T&gt; 之类的接口,反之亦然。

                  IEnumerable&lt;T&gt;IList&lt;T&gt; 之类的接口视为比萨饼(意大利辣香肠、蘑菇、黑橄榄...)中的单个成分,以及具体类 List&lt;T&gt; 作为披萨List&lt;T&gt; 实际上是一个Supreme Pizza,它始终包含所有接口成分的组合(ICollection、IEnumerable、IList 等)。

                  就披萨及其浇头而言,您获得的内容取决于您在内存中创建其对象引用时“键入”列表的方式。您必须按如下方式声明您正在烹饪的比萨饼类型:

                  // Pepperoni Pizza: This gives you a single Interface's members,
                  // or a pizza with one topping because List<T> is limited to acting like an IEnumerable<T> type.
                  
                  IEnumerable<string> pepperoniPizza = new List<string>();
                  
                  
                  // Supreme Pizza: This gives you access to ALL 8 Interface members combined
                  // or a pizza with ALL the ingredients because List type uses all Interfaces!!
                  
                  List<string> supremePizza = new List<string>();
                  

                  请注意,您不能将接口实例化为自身(或吃生的意大利辣香肠)。当您将List&lt;T&gt; 实例化为一种接口类型或IEnumerable&lt;T&gt; 时,您只能访问其实现并获得带有一个浇头的意大利辣香肠披萨。您只能访问IEnumerable&lt;T&gt; 成员,而无法查看List&lt;T&gt; 中的所有其他接口成员。当List&lt;T&gt; 被实例化为List&lt;T&gt; 时,它实现了所有8 个接口,因此它可以访问它已实现的所有接口的所有成员(或Supreme Pizza toppings)!

                  这是List&lt;T&gt; 类,向您展示WHY。请注意 .NET 库中的 List&lt;T&gt; 已实现所有其他接口!注意IEnumerable&lt;T&gt; 只是它实现的所有接口成员的一小部分。

                  public class List<T> :
                  
                      ICollection<T>,
                      IEnumerable<T>,
                      IEnumerable,
                      IList<T>,
                      IReadOnlyCollection<T>,
                      IReadOnlyList<T>,
                      ICollection,
                      IList
                  
                  {
                  public List();
                  public List(IEnumerable<T> collection);
                  public List(int capacity);
                  public T this[int index] { get; set; }
                  public int Count { get; }
                  public int Capacity { get; set; }
                  public void Add(T item);
                  public void AddRange(IEnumerable<T> collection);
                  public ReadOnlyCollection<T> AsReadOnly();
                  public bool Exists(Predicate<T> match);
                  public T Find(Predicate<T> match);
                  public void ForEach(Action<T> action);
                  public void RemoveAt(int index);
                  public void Sort(Comparison<T> comparison);
                  
                  // ......and much more....
                  
                  }
                  

                  那么为什么不始终将List&lt;T&gt; 实例化为List&lt;T&gt;

                  List&lt;T&gt; 实例化为List&lt;T&gt; 可让您访问所有接口成员! 但您可能不需要所有内容。选择一种接口类型允许您的应用程序存储具有较少成员的较小对象并保持您的应用程序紧凑。谁每次都需要Supreme Pizza

                  但使用接口类型还有一个第二个原因:灵活性。因为 .NET 中的其他类型,包括您自己的自定义类型,可能使用相同的“流行”接口类型,这意味着您可以稍后将您的 List&lt;T&gt; 类型替换为任何其他实现的类型,例如 IEnumerable&lt;T&gt;。如果您的变量是一个接口类型,您现在可以切换出使用List&lt;T&gt; 以外的其他对象创建的对象。依赖注入是使用接口而不是具体类型的这种灵活性的一个很好的例子,以及为什么您可能希望使用接口创建对象。

                  【讨论】:

                    猜你喜欢
                    • 2011-07-03
                    • 2010-12-19
                    • 2011-03-03
                    • 1970-01-01
                    • 1970-01-01
                    • 2021-12-12
                    • 1970-01-01
                    • 1970-01-01
                    • 2011-03-11
                    相关资源
                    最近更新 更多