当我编写返回一组项目的 DAL 或其他代码时,我是否应该始终使我的返回语句为:
public IEnumerable<FooBar> GetRecentItems()
或者public IList<FooBar> GetRecentItems()
目前,在我的代码中,我一直尽可能地使用IEnumerable,但我不确定这是否是最佳实践?它似乎是正确的,因为我返回了最通用的数据类型,同时又描述了它的作用,但也许这样做是不正确的。
当我编写返回一组项目的 DAL 或其他代码时,我是否应该始终使我的返回语句为:
public IEnumerable<FooBar> GetRecentItems()
或者public IList<FooBar> GetRecentItems()
目前,在我的代码中,我一直尽可能地使用IEnumerable,但我不确定这是否是最佳实践?它似乎是正确的,因为我返回了最通用的数据类型,同时又描述了它的作用,但也许这样做是不正确的。
框架设计指南建议在需要返回可被调用者修改的集合时使用 Collection 类,而对于只读集合则应使用 ReadOnlyCollection。
之所以推荐这么做而不是简单地使用 IList
是因为 IList
没有告知调用者它是否为只读。
如果您返回一个 IEnumerable<T>
,那么对于调用者来说,某些操作可能会更加麻烦。此外,您也不再能够为调用者提供修改集合的灵活性,这取决于您是否需要该功能。
请记住,LINQ拥有一些技巧,会根据执行的对象类型来优化某些调用。例如,如果执行的是 Count
并且底层集合是 List,则不会遍历所有元素。
就我个人而言,在ORM中我可能会使用 Collection<T>
作为返回值。
IList<T>
有几个方法不存在于IEnumerable<T>
中:IndexOf(T item)
- Insert(int index, T item)
- RemoveAt(int index)
并且还有属性:T this[int index] { get; set; }
如果您需要这些方法,那么请返回IList<T>
。总的来说,您应该要求最通用的并返回最具体的内容。因此,如果您有一个需要参数的方法,并且您只需要 IEnumerable 中可用的内容,那么这应该是您的参数类型。如果您的方法可以返回 IList 或 IEnumerable,则更喜欢返回 IList。这确保它可以被最广泛的消费者使用。
在所需内容上放松,并在提供内容时明确。
这取决于...
返回最不具体的类型(IEnumerable
)将使您在未来更改底层实现时具有最大的灵活性。
返回更具体的类型(IList
)将为您的API用户提供更多的操作结果。
我总是建议返回最不具体的类型,该类型具有您的用户需要的所有操作...因此,您首先必须确定在定义API上下文中,对结果进行哪些操作是有意义的。
需要考虑的一个问题是,如果您正在使用延迟执行的LINQ语句来生成IEnumerable<T>
,那么在从方法返回之前调用.ToList()
可能会导致您的项被迭代两次——一次创建列表,一次当调用者循环遍历、筛选或转换您的返回值时。实际上,我喜欢在必须之前避免将LINQ-to-Objects的结果转换为具体的List或Dictionary。如果我的调用者需要一个List,那只需要一个简单的方法调用 - 我不需要为他们做出这个决定,并且在调用者只是使用foreach的情况下,这使得我的代码稍微更加高效。
List<T>
提供了许多更多的功能,例如修改返回的对象以及通过索引访问。因此问题归结为:在应用程序的具体用例中,您是否希望支持这些用途(通常是通过返回新构建的集合!),以方便调用者 - 还是在所有调用者需要的仅仅是遍历集合的简单情况下提供速度,因此您可以安全地返回对实际基础集合的引用,而不必担心这将导致其被错误更改等。
只有您能回答这个问题,并且只有通过充分了解您的调用者将要如何使用返回值以及性能在这里的重要性(您将要复制多大的集合,这可能成为一个瓶颈等)才能回答这个问题。
List
是 IEnumerable
,但你可以进行计数功能、添加元素和删除元素。IEnumerable
不是很有效率。Parent
控制集合的修改,则仅为了获取 Count
返回一个 IList
不是一个好主意。IEnumerable<T>
有一个 Count()
扩展方法,在 CLR 中,如果底层类型是 IList
,则会快捷地转换为 .Count
,因此性能差异可以忽略不计。IEnumerable
,如果需要进行添加操作,则将这些方法添加到父类中,否则消费者就在 Model 中管理集合,这违反了原则,例如 manufacturer.Models.Add(model)
违反了 Demeter 法则。当然,这些只是指导方针而不是硬性规定,但在完全掌握适用性之前,盲目遵循比不遵循要好。public interface IManufacturer
{
IEnumerable<Model> Models {get;}
void AddModel(Model model);
}
List
)作为返回值,对于输入参数即使是集合类型也要使用最通用的类型。IReadOnlyList
或IReadOnlyCollection
作为返回值类型来显示它。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>
。依赖注入是使用接口而不是具体类型的这种灵活性的很好的例子,这也是为什么您可能希望使用接口创建对象的原因。