为什么在多对多/一对多关系中使用ICollection而不是IEnumerable或List<T>?

444
我在很多教程中看到过这样的代码,导航属性是ICollection<T>类型。
这是Entity Framework的强制要求吗?我能否使用IEnumerable
使用ICollection的主要目的是什么,相对于IEnumerable或甚至List<T>而言?

所以答案是,不一定要使用ICollection。如果只需要迭代,我可以使用IEnumerable,对吧? - deathrace
10个回答

509
通常,你选择的类型取决于你需要访问哪些方法。一般来说,对于只需要迭代的对象列表,使用 IEnumerable<>(MSDN:http://msdn.microsoft.com/en-us/library/system.collections.ienumerable.aspx);对于需要迭代并修改的对象列表,使用 ICollection<>(MSDN:http://msdn.microsoft.com/en-us/library/92t2ye13.aspx);对于需要迭代、修改、排序等的对象列表,使用 List<>(有关完整列表,请参见此处:http://msdn.microsoft.com/en-us/library/6sh2ey19.aspx)。
从更具体的角度来看,延迟加载与选择类型有关。默认情况下,Entity Framework 中的导航属性带有更改跟踪并且是代理。为了将动态代理创建为导航属性,虚拟类型必须实现ICollection

表示关系的“多”端的导航属性必须返回一个实现ICollection的类型,其中T是关系另一端的对象类型。-创建POCO代理的要求MSDN

有关定义和管理关系的更多信息MSDN

3
所以,这样一来,“List”应该会更好,是吧? - Jan Carlo Viray
3
我很喜欢使用“List”。尽管它是开销最大的,但它提供了最多的功能。 - Travis J
1
列表更多地是通过它们的索引器来定义的,而不是通过排序它们的能力(拥有整数索引器使得对某些东西进行排序变得容易,但这并不是必需的)。 - phoog
2
关于您的编辑,将属性限制为接口类型并不是关于内存,而是关于封装。请考虑以下示例:private IEnumerable<int> _integers = new List<int> { 1, 2, 3 };private List<int> _integers = new List<int> { 1, 2, 3 }; 使用相同的内存。 - phoog
14
List<T>类有一个GetEnumerator()方法,与其实现的IEnumerable<T>接口分离,该方法返回可变结构类型List<T>.Enumerator。在大多数情况下,该类型将比独立的堆对象提供稍微更好的性能。对于那些鸭式枚举器的编译器(如C#和vb.net),可以利用此功能生成foreach代码。如果在foreach之前将List<T>强制转换为IEnumerable<T>,则IEnumerable<T>.GetEnumerator()方法将返回一个分配在堆上的对象,从而使优化无法实现。 - supercat
显示剩余7条评论

114

ICollection<T> 被使用是因为 IEnumerable<T> 接口没有提供添加、删除或修改集合的方法。


4
与 List<T> 进行比较怎么样? - Jan Carlo Viray
17
List<T> 实现了 ICollection<T> - spender
4
非泛型的 ICollection 接口不允许添加项,但它仍然是 IEnumerable<T> 的一个有用补充,因为它提供了一个 Count 成员,通常比枚举所有项要快得多。请注意,如果将一个 IList<Cat>ICollection<Cat> 传递给期望一个 IEnumerable<Animal> 的代码,则如果它实现了非泛型的 ICollection,则 Count() 扩展方法会很快,但如果它只实现了泛型接口,则不会,因为典型的 ICollection<Cat> 不会实现 ICollection<Animal> - supercat

70

针对您的问题有关于 List<T>

List<T> 是一个类,指定一个接口可以提供更多的实现灵活性。更好的问题是“为什么不用 IList<T>?”

回答这个问题,考虑一下 IList<T> 相比 ICollection<T> 添加了什么:整数索引,这表示项目具有某种任意顺序,并且可以通过引用该顺序来检索它们。在大多数情况下,这可能没有意义,因为在不同的上下文中可能需要以不同的方式排序项目。


40

ICollection和IEnumerable之间有一些基本的差异

  • IEnumerable - 只包含GetEnumerator方法以获取枚举器并允许遍历
  • ICollection 包含额外的方法:Add,Remove,Contains,Count,CopyTo
  • ICollection 继承自IEnumerable
  • 使用ICollection可以通过像添加/删除等方法来修改集合。您不能使用IEnumerable进行相同的操作。

简单程序:

using System;
using System.Collections;
using System.Collections.Generic;

namespace StackDemo
{
    class Program 
    {
        static void Main(string[] args)
        {
            List<Person> persons = new List<Person>();
            persons.Add(new Person("John",30));
            persons.Add(new Person("Jack", 27));

            ICollection<Person> personCollection = persons;
            IEnumerable<Person> personEnumeration = persons;

            // IEnumeration
            // IEnumration Contains only GetEnumerator method to get Enumerator and make a looping
            foreach (Person p in personEnumeration)
            {                                   
               Console.WriteLine("Name:{0}, Age:{1}", p.Name, p.Age);
            }

            // ICollection
            // ICollection Add/Remove/Contains/Count/CopyTo
            // ICollection is inherited from IEnumerable
            personCollection.Add(new Person("Tim", 10));

            foreach (Person p in personCollection)
            {
                Console.WriteLine("Name:{0}, Age:{1}", p.Name, p.Age);        
            }
            Console.ReadLine();

        }
    }

    class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public Person(string name, int age)
        {
            this.Name = name;
            this.Age = age;
        }
    }
}

22

我记得是这样的:

  1. IEnumerable有一个GetEnumerator()方法,允许读取集合中的值但无法写入它。在C#中,使用foreach语句可以为我们处理大部分枚举器的复杂性。IEnumerable有一个属性:Current,它返回当前元素。

  2. ICollection实现了IEnumerable并添加了一些额外的属性,其中最常用的是Count。泛型版本的ICollection实现了Add()和Remove()方法。

  3. IList实现了IEnumerable和ICollection,并添加了整数索引访问项目的功能(通常不需要,因为排序是在数据库中完成的)。


4
根据你写的内容,ICollection和IList是相同的。请补充说明在IList中添加了什么,在ICollection中不存在。 - bets
ICollection VS IList,IList是System.Collection中唯一一个包含IEnumerable和ICollection所有功能以及额外功能的接口。IList具有Insert和Remove方法,两种方法都在其参数中接受索引。因此,它支持基于索引的集合操作。 - E.Meir

8
使用ICollection的基本思想是提供界面来只读地访问一些有限的数据。实际上,你有一个ICollection.Count 属性。IEnumerable更适合于一些链式数据,您可以在那里阅读到某个逻辑点,某个由使用者明确指定的条件或枚举结束。

17
今天我学到,ICollection 是只读的,而 ICollection<T> 则不是。 - Carl G

2

2
让我们通过逻辑思考来超越常规,清楚地理解您问题中的这三个接口:
当某个实例的类实现了System.Collection.IEnumerable接口时,简单来说,我们可以说该实例既可枚举又可迭代,这意味着该实例允许在单个循环中遍历其包含的所有项目和元素。
这意味着也可以枚举该实例所包含的所有项目和元素。
实现System.Collection.IEnumerable接口的每个类还实现了GetEnumerator方法,该方法不带参数并返回一个System.Collections.IEnumerator实例。
System.Collections.IEnumerator接口的实例行为与C++迭代器非常相似。
当某个实例的类实现了System.Collection.ICollection接口时,简单来说,我们可以说该实例是一些东西的集合。
该接口的泛型版本,即System.Collection.Generic.ICollection,更具说明性,因为该泛型接口明确说明了集合中的事物类型。
这都是合理、理性、逻辑和有意义的,因为System.Collections.ICollection接口继承自System.Collections.IEnumerable接口,理论上每个集合也都是可枚举和可迭代的,理论上可以遍历每个集合中的所有项目和元素。
System.Collections.ICollection接口表示一组可变的有限动态集合,这意味着可以从集合中删除已存在的项目并将新项目添加到同一集合中。
这解释了为什么System.Collections.ICollection接口具有“Add”和“Remove”方法。
由于System.Collections.ICollection接口的实例是有限集合,因此“有限”一词意味着该接口的每个集合始终具有有限数量的项目和元素。
System.Collections.ICollection接口的属性Count应返回此数字。
System.Collections.IEnumerable接口没有System.Collections.ICollection接口具有的这些方法和属性,因为System.Collections.IEnumerable接口具有这些方法和属性没有任何意义。
逻辑还表明,既可枚举又可迭代的每个实例不一定是集合,也不一定是可变的。
当我说可变时,我的意思是不要立即认为您可以从既可枚举又可迭代的东西中添加或删除某些内容。
例如,如果我刚刚创建了一些质数的有限序列,那么这些质数的有限序列确实是System.Collections.IEnumerable接口的一个实例,因为现在我可以在单个循环中遍历此有限序列中的所有质数,并对每个质数执行所需操作,例如将它们打印到控制台窗口或屏幕上,但这些质数的有限序列不是System.Collections.ICollection接口的一个实例,因为将复合数添加到这些质数的有限序列中是没有意义的。
此外,如果您想在下一次迭代中获取当前迭代中最接近的更大的质数,则还不希望从这些质数的有限序列中删除已存在的质数。
你可能想在System.Collections.IEnumerable接口的GetEnumerator方法中使用“yield return”来生成质数,而不会在内存堆上分配任何内容,并且不会将垃圾回收器(GC)用于从堆中释放此内存,因为这显然是操作系统内存浪费和降低性能。

当调用System.Collections.ICollection接口的方法和属性时,应在堆上执行动态内存分配和释放,但在调用System.Collections.IEnumerable接口的方法和属性时不应该执行(尽管System.Collections.IEnumerable接口只有1个方法和0个属性)。

根据其他人在此Stack Overflow网页上所说,System.Collections.IList接口仅表示可排序集合,这解释了为什么System.Collections.IList接口的方法使用索引,而System.Collections.ICollection接口的方法则不同。

简而言之,System.Collections.ICollection接口并不意味着它的实例是可排序的,但System.Collections.IList接口确实意味着它是可排序的。

理论上,有序集合是无序集合的特殊情况。

这也很有道理,解释了为什么System.Collections.IList接口继承System.Collections.ICollection接口。


2
我过去的做法是使用、或(如果是静态列表)来声明我的内部类集合,具体取决于我是否需要在存储库中的某个方法中执行以下操作之一: 枚举,排序/订购或修改。当我只需要枚举(和可能进行排序)对象时,我会创建一个临时的以便在IEnumerable方法中处理该集合。我认为这种做法只在集合相对较小的情况下才有效,但它可能是一个好的通用实践,不确定。如果有证据表明这不是好的实践,请纠正我。

-1
谷歌搜索带我来到这里,我还没有看到有人提到这个。我刚刚被一个与下面类似的API搞糊涂了:
void Load(object key)
void Load(IEnumerable keys)

传递一个字符串会选择IEnumerable重载,这不是我(也不是API作者)的意图。如果IEnumerable是ICollection,那么就会选择预期的重载。

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