我应该总是返回IEnumerable<T>而不是IList<T>吗?

127

当我编写返回一组项目的 DAL 或其他代码时,我是否应该始终使我的返回语句为:

public IEnumerable<FooBar> GetRecentItems()
或者
public IList<FooBar> GetRecentItems()

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


2
你是在问 List<T> 还是 IList<T>?标题和问题说的不一样... - Fredrik Mörk
尽可能使用用户界面IEnumerable或IList而不是具体类型。 - Usman Masood
2
我将集合作为List<T>返回。我认为没有必要返回IEnumberable<T>,因为你可以从List<T>中提取它。 - Chuck Conway
1
请参见:https://dev59.com/I3RC5IYBdhLWcg3wMeBS - Mauricio Scheffer
可能是 ienumerablet-as-return-type 的重复问题。 - nawfal
15个回答

79

框架设计指南建议在需要返回可被调用者修改的集合时使用 Collection 类,而对于只读集合则应使用 ReadOnlyCollection

之所以推荐这么做而不是简单地使用 IList 是因为 IList 没有告知调用者它是否为只读。

如果您返回一个 IEnumerable<T> ,那么对于调用者来说,某些操作可能会更加麻烦。此外,您也不再能够为调用者提供修改集合的灵活性,这取决于您是否需要该功能。

请记住,LINQ拥有一些技巧,会根据执行的对象类型来优化某些调用。例如,如果执行的是 Count 并且底层集合是 List,则不会遍历所有元素。

就我个人而言,在ORM中我可能会使用 Collection<T> 作为返回值。


20
《收藏指南》包含更详细的“可做”和“不可做”的列表。 - Chaquotay

51
这实际上取决于您使用特定接口的原因。例如,IList<T>有几个方法不存在于IEnumerable<T>中:
- IndexOf(T item) - Insert(int index, T item) - RemoveAt(int index) 并且还有属性:
- T this[int index] { get; set; } 如果您需要这些方法,那么请返回IList<T>

3
@Jon FDG建议使用Collection<T>或者ReadOnlyCollection<T>作为集合类型的返回值,请参考我的回答。 - Sam Saffron
当你说“它将拯救CLR”时,在你的最后一句话中,你的意思不是很清楚。使用IEnumerable与IList会拯救什么?你能让这更清晰吗? - PositiveGuy
2
@CoffeeAddict 三年后我认为你是对的——最后一部分很模糊。如果一个期望参数为IList<T>的方法得到了IEnumerable<T>,那么IEnumerable必须手动包装在新的List<T>或其他IList<T>实现者中,而CLR不会为您完成这项工作。相反,期望参数为IEnumerable<T>的方法得到了IList<T>,可能需要进行一些拆箱操作,但事后看来可能不需要,因为IList<T>实现了IEnumerable<T>。 - Jon Limjap

49

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

在所需内容上放松,并在提供内容时明确。


7
接受一般类型作为输入的理由是为了让您能够使用最广泛的输入范围,以便尽可能多地重复使用组件。另一方面,既然您已经知道手边有什么样的对象,遮掩它就没有太多意义。 - Mel
1
我认为我同意使用更通用的参数,但是您为什么要返回不太通用的内容呢? - Dave Cousineau
15
好的,让我用另一种方式尝试。为什么你要丢掉信息?如果你只关心结果是IEnumerable<T>,那么知道它是IList<T>会对你造成伤害吗?不会。在某些情况下,这可能是多余的信息,但它不会对你造成任何损害。接下来是好处。如果你返回一个List或IList,我可以立即知道该集合已经被检索,而这是我通过IEnumerable无法知道的。这可能是有用的信息,也可能没有,但再次问一遍,为什么你要丢掉信息?如果你了解某些东西的额外信息,请传递给别人。 - Mel
2
我不确定这是否正确。许多LINQ方法执行惰性计算并返回“IEnumerable”。不能保证数据已被枚举。 - Nick Udell
1
我会继续强调这一点。我意识到我可以更清楚地表达。IEnumerable不一定已经被评估过了。IQueryable和IEnumerable都支持延迟执行。主要区别在于,附加到IQueryable上的条件如果来自ORM,则仍然可以进入SQL查询。IEnumerable可能尚未评估,但添加到其上的其他谓词将不会进入查询,并且将在初始检索后应用第二轮。现在...我认为这更清晰了。 - Mel
显示剩余3条评论

29

这取决于...

返回最不具体的类型(IEnumerable)将使您在未来更改底层实现时具有最大的灵活性。

返回更具体的类型(IList)将为您的API用户提供更多的操作结果。

我总是建议返回最不具体的类型,该类型具有您的用户需要的所有操作...因此,您首先必须确定在定义API上下文中,对结果进行哪些操作是有意义的。


好的通用答案,适用于其他方法。 - user420667
1
我知道这个答案很古老... 但它似乎与文档相矛盾:“如果既不满足IDictionary<TKey, TValue>接口,也不满足IList<T>接口的要求,则应从ICollection<T>接口派生新的集合类以获得更大的灵活性”。这似乎意味着更具体的类型应该更受推荐。 (https://msdn.microsoft.com/zh-cn/library/92t2ye13(v=vs.110).aspx) - DeborahK
1
问题是,你不知道用户可能需要什么。那么为什么不返回你所拥有的呢?如果你有一个List并且你返回了一个IEnumerable,你只是让调用者受到了限制。如果他需要一个List,他将不得不遍历整个集合以获取列表...而它本来就是一个List。如果他只需要一个IEnumerable,他可以像处理一个没有问题的List一样处理它。我不明白为什么不返回最可用的对象。 - NPearson

20

需要考虑的一个问题是,如果您正在使用延迟执行的LINQ语句来生成IEnumerable<T>,那么在从方法返回之前调用.ToList()可能会导致您的项被迭代两次——一次创建列表,一次当调用者循环遍历、筛选或转换您的返回值时。实际上,我喜欢在必须之前避免将LINQ-to-Objects的结果转换为具体的List或Dictionary。如果我的调用者需要一个List,那只需要一个简单的方法调用 - 我不需要为他们做出这个决定,并且在调用者只是使用foreach的情况下,这使得我的代码稍微更加高效。


@Joel Mueller,我通常会在它们上面调用ToList()。我一般不喜欢将IQueryable暴露给我的项目的其他部分。 - KingNestor
5
我更多地是指LINQ-to-Objects,其中IQueryable通常不涉及。当涉及到数据库时,ToList()变得更加必要,因为否则你在迭代之前有关闭连接的风险,这样做不太好。然而,当这不是问题时,很容易将IQueryable作为IEnumerable公开,而不会强制进行额外的迭代,当你想隐藏IQueryable时。 - Joel Mueller
我不需要为他们做出决定,在调用方仅执行 foreach 的情况下,这使我的代码略微更高效。但你并不知道调用方会做什么,如果他迭代两次呢?那么你的延迟操作将被执行两次。 - spartaco

10
List<T> 提供了许多更多的功能,例如修改返回的对象以及通过索引访问。因此问题归结为:在应用程序的具体用例中,您是否希望支持这些用途(通常是通过返回新构建的集合!),以方便调用者 - 还是在所有调用者需要的仅仅是遍历集合的简单情况下提供速度,因此您可以安全地返回对实际基础集合的引用,而不必担心这将导致其被错误更改等。

只有您能回答这个问题,并且只有通过充分了解您的调用者将要如何使用返回值以及性能在这里的重要性(您将要复制多大的集合,这可能成为一个瓶颈等)才能回答这个问题。


“安全地返回对真实基础集合的引用,而不必担心会出现错误更改” - 即使您返回IEnumerable<T>,他们是否仍然可以将其强制转换回List<T>并进行更改? - Kobi
并非每个IEnumarable<T>都是List<T>。如果返回的对象不是从List<T>继承或实现IList<T>接口的类型,则会导致InvalidCastException异常。 - lowglider
2
List<T> 存在一个问题,它会将你锁定到特定的实现中。建议使用 Collection<T> 或 ReadOnlyCollection<T>。 - Sam Saffron

5
我认为你可以使用任意一种,但每种都有用途。基本上,ListIEnumerable,但你可以进行计数功能、添加元素和删除元素。
对于计算元素的数量,IEnumerable 不是很有效率。
如果集合旨在为只读,或者通过 Parent 控制集合的修改,则仅为了获取 Count 返回一个 IList 不是一个好主意。
在 Linq 中,IEnumerable<T> 有一个 Count() 扩展方法,在 CLR 中,如果底层类型是 IList,则会快捷地转换为 .Count,因此性能差异可以忽略不计。
通常我认为(个人意见)在可能的情况下最好返回 IEnumerable,如果需要进行添加操作,则将这些方法添加到父类中,否则消费者就在 Model 中管理集合,这违反了原则,例如 manufacturer.Models.Add(model) 违反了 Demeter 法则。当然,这些只是指导方针而不是硬性规定,但在完全掌握适用性之前,盲目遵循比不遵循要好。
public interface IManufacturer 
{
     IEnumerable<Model> Models {get;}
     void AddModel(Model model);
}

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

4
当涉及到返回值而非输入参数时,情况就不那么简单了。对于输入参数,你清楚地知道需要做什么。因此,如果需要能够遍历集合,则采用IEnumberable,而如果需要添加或删除,则采用IList。
但是对于返回值来说,情况就更加复杂了。你的调用者期望什么?如果你返回一个IEnumerable,则他事先不知道可以将其转换为IList。但是,如果你返回一个IList,则他会知道可以遍历它。因此,在决定返回什么时,必须考虑到你的调用者将要对数据执行什么操作。你的调用者需要/期望的功能应该是决定返回什么的依据。

1

TL; DR; – 摘要

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

更多信息


0

IEnumerable<T> 包含 List<T> 中的一小部分内容,List<T> 包含与 IEnumerable<T> 相同的内容但更多!只有在需要较小功能集合时才使用 IEnumerable<T>。如果计划使用更大、更丰富的功能集,请使用 List<T>

披萨解释

以下是为什么在 C 语言(如 Microsoft C#)中实例化对象时要使用类似于 IEnumerable<T>List<T> 这样的接口的一个更全面的解释。

将接口(如IEnumerable<T>IList<T>)视为披萨中的单个配料(意大利辣香肠、蘑菇、黑橄榄等),而具体类(如List<T>)则是整个披萨。实际上,List<T>是一种至尊披萨,它始终包含所有接口成分的组合(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!!

IList<string> supremePizza = new List<string>();

请注意,您不能将接口本身实例化(或食用生的意大利辣香肠)。当您将List<T>作为一个接口类型,例如IEnumerable<T>进行实例化时,您只能访问其实现并获得一种配料的意大利辣香肠披萨。您只能访问IEnumerable<T>成员,并且无法看到List<T>中的所有其他接口成员。

List<T>作为IList<T>进行实例化时,它实现了所有8个接口,因此可以访问其已实现的所有接口的成员(或至高无上的披萨配料)!

这是List<T>类,向您展示了为什么会这样。请注意,.NET库中的List<T>已经实现了包括IList在内的所有其他接口!!但是,IEnumerable<T>仅实现了这些列表接口成员的一小部分。

public class List<T> :

    ICollection<T>,
    IEnumerable<T>,
    IEnumerable,
    IList<T>,
    IReadOnlyCollection<T>,
    IReadOnlyList<T>,
    ICollection,
    IList

{
// List<T> types implement all these goodies and more!
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<T>实例化为List<T>呢?

List<T>实例化为List<T>可以让您访问所有接口成员!但您可能并不需要所有内容。选择一个接口类型允许您的应用程序存储一个更小的对象,具有较少的成员,并保持应用程序的紧凑性。谁每次都需要至尊比萨呢?

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


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