了解您的集合:从 IEnumerable 到 List 及以后
理清集合之间的混淆
引言
我曾多次看到方法参数和返回值不合理,并讨厌在某处更改集合类型导致我不得不在不同地方更改方法签名。
我想写这篇文章很久了,以帮助揭开一些集合及其含义周围的神秘光环
注意:您可以通过单击此处直接跳转到末尾的比较图。
关于泛型的一点说明
泛型在 .Net 2 中引入,普通列表是 List
,泛型列表则看起来像 List<T>
,其中 T 是一个类型(在以下示例中,我使用 Toy
作为类型)。泛型版本将只接受您定义的类型的成员,因此,如果您定义了一个 int 列表:List<int>
,您就不能向其中添加字符串,只能添加 int。
在本文中,我主要讨论泛型版本,但我也会指出非泛型版本,以避免混淆并澄清我的意思(感谢 Georgi)。
我们的“目标”类比
让我们设想一个场景,涉及一位父亲(您作为开发者)、一个儿子、一项不情愿的任务(打扫房间)以及一个悬而未决的胡萝卜(承诺的奖励)。
大意是这样的:父亲承诺如果儿子打扫房间就给他一个玩具(儿子显然不情愿这样做)。
注意:请不要从本文中获取育儿建议。我对此概不负责。
热身
集合是一堆相关的对象。为了不将其与实际的MSDN Collection 类混淆,我将使用斜体表示概念。将相关的对象视为该类型的集合会使我们的生活更轻松、更简单。
假设我们有以下玩具结构
struct Toy
{
public string Name { get; set; }
public int Cost { get; set; }
public NoiseLevel NoiseLevel { get; set; }
}
以及这个枚举
enum NoiseLevel
{
Quiet , // You'll never hear it
Normal , // It's a toy
Noisy , // You'll secretly take the batteries away
BanFriend , // If somebody gave this to your offspring, you'll never talk to them again
}
如果我们处理的是一堆玩具(无论是购买、销售、寻找最贵的,还是决定哪些朋友可以被邀请吃晚餐),我们可以将它们捆绑在一起,以便于处理。
以下是一些我们可以使用的集合
Toy[] toyArray = new Toy[10];
Collection<Toy> toyCollection = new Collection<Toy>(); // Generic
List<Toy> toyList = new List<Toy>(10); // Generic
IEnumerable<T> 和 IEnumerator<T>(以及非泛型对应项)
集合的常用用途是处理其成员。集合最基本的接口是 IEnumerable<T> 接口。它唯一的方法是 GetEnumerator
,它返回一个 IEnumerator<T>,允许您遍历集合。
这是集合最“抽象”的概念,它只允许您遍历它。
如果您不熟悉 IEnumerator,它类似于在以下简单 for 循环中拥有一个 counter
Toy[] toyArray = new Toy[10];
for (int counter = 0; counter < 10; counter++)
{
toyArray[counter] = new Toy();
}
我还是建议您点击链接阅读关于 IEnumerator
的内容。
IEnumerator
有一个 Current
属性(我指向哪里),它的方法是 MoveNext
和 Reset
。
因此,如果您正在处理任何实现了 IEnumerable
的对象(无论是泛型还是非泛型),您就知道您可以获取当前对象,并且您可以向前移动或从零开始。最简单有用的例子是使用 Enumerable.Range 方法
var one_to_ten = Enumerable.Range(1, 10);
这将返回一个 Int
类型的 IEnumerable
,其中包含数字 1 到 10(注意:这是惰性的,如果您不使用它,它将不会被调用)。
一个(有点性别歧视的)例子是一个女人和她的鞋子。她不关心有多少双,只要她能遍历所有鞋子,每天挑下一双。因为我觉得这不好,所以男性对应的是一个男人和他的电子游戏。外面有成千上万的游戏,但只要他能玩下一个,他就很开心。
注意:在这个级别上,泛型版本和非泛型版本之间唯一的真正区别是,非泛型版本有 4 个扩展方法(AsParallel
、AsQueryable
、Cast<TResult>
和 OfType<TResult>
)。泛型版本有更多(多到我在这里就不一一列举了)。请随意查看 MSDN 页面或您的 Intellisense。
您可以在官方 MSDN 页面上阅读更多内容:泛型版本,非泛型版本。
ICollection<T>
这是下一个级别。它实现了 IEnumerable<T>
和 IEnumerable
,并将添加以下属性:Count
、IsReadOnly
。
您可以与它一起使用的新方法是:Add
、Clear
、Contains
、CopyTo
和 Remove
。
这比 Enumerator
更具体一些,您可以对其进行操作、添加、删除以及检查某个对象是否存在于该集合中。如果您不关心对象在集合中的位置,那么这就是您想要使用的。
例如:鸡蛋在蛋托里。烘焙时,您想知道总共有 6 个鸡蛋,并想将它们加入面团,但您不关心它们的实际位置或放入的顺序。
在我看来,ICollection
(非泛型)接口不是很实用。它添加了CountIsSynchronized
和 SyncRoot
属性,以及仅有的 CopyTo()
方法,该方法允许您将集合复制到数组中。
您可以在 MSDN 上的官方页面上阅读更多内容:泛型版本,非泛型版本。
IList<T>
此接口同时实现了 IEnumerable<T>
和 ICollection<T>
,并将添加以下属性:Item
。
您可以使用的Buongiorno新方法是:IndexOf
、Insert
、RemoveAt
。
此接口让您对集合有更“真实”的感觉,您可以查看哪个对象在哪里,在特定索引处添加,以及按索引删除特定项。
例如:您正在和朋友打牌,您知道牌堆中的第五张牌是鬼牌。知道它在牌堆中还不够。如果您道德败坏,下次您给自己抽牌时,您将使用 RemoveAt(4)
。
非泛型接口同时实现了 IEnumerable
和 ICollection
,并将添加以下属性:IsFixedSize
、IsReadOnly
和 Item
。
方法方面,非泛型接口看起来与其泛型对应项(共 7 个新方法),但其中 4 个只是非泛型 ICollection
中未实现的方法。
您可以在 MSDN 上的官方页面上阅读更多内容:泛型版本和非泛型版本。
ISet<T>
此接口提供了实现集合(集合,具有唯一元素和特定操作)的方法。 HashSet<T> 和 SortedSet<T> 集合实现了此接口。
这对于处理集合的数学问题很有用。请访问此 MSDN 页面了解更多信息,并请注意,此接口没有非泛型版本(即没有 ISet
)。
我添加此内容是因为在比较屏幕截图中,我添加了与我们讨论过的其他集合的区别。
Enumerable<T>、Collection<T>、List<T>
这些是您通常会使用的集合类,它们只是实现了我们之前讨论过的接口。
数组
数组是一个类,它实现了:ICloneable
、IList
、ICollection
、IEnumerable
、IStructuralComparable
、IStructuralEquatable
.
它不是 System.Collections
的一部分,并且其大小是固定的。数组可能是“集合”最古老的形式,但由于其实现方式在不同版本的 .Net 中有所不同,因此它们存在一些细微的问题。数组不是泛型的,但它们也有点泛型。关于为什么没有泛型数组的冗长(但有趣的)回答,请访问 stack overflow 上的这个问题。
当处理特定数量的对象时,您通常会想要一个数组。例如,您只允许从图书馆借 5 本书。一个大小为 5 的 Book[]
是一个很好的表示方式。另一种选择是当您从方法或库中获取数组时。
根据 MSDN,当不需要 Add
和 Remove
时,也是一些好情况。
您可以在MSDN上的官方页面上阅读更多内容,并务必查看数组用法指南。
其余的集合
您可以在 System.Collections 命名空间页面上找到 .Net 中其余的集合类型。
可视化辅助和总结
每个属性/方法都将具有其定义所在父集合的颜色。例如,Item
属性是绿色的,因为它定义在 IList<T>
中,而在同一列中,Count
是蓝色的,因为它定义在 ICollection<T>
接口上。
这是泛型比较表
这是非泛型对应项
起飞
现在我们已经涵盖了基础知识,我们将回到“何时使用什么”的问题,并使用我们的类比。
如果您是父亲,而您的儿子不愿意打扫房间,您可能会想贿赂他,并为他做他本应做的奖励。哪种选择看起来更好?
- 如果你打扫房间,我会给你一个玩具。
- 如果你打扫房间,我会给你一套
Pearl Drums
和一套配套服装。
假设您的孩子信任您,而您选择了第二种选择,那么您就麻烦了(除非您有钱买鼓并且没有邻居)。如果您选择了第一种选择,您可以给他买一个漂亮的独角兽橡皮鸭,确保他长大后会成为一名出色的开发者(如果您不知道为什么,请阅读这个和这个)。
故事的寓意是:不要承诺您无法兑现的东西,并尽量含糊其辞。这也意味着最好接受和返回一个接口而不是具体的实现。例如
// Good:
public IList<int> GetMeSomeInts() {...};
// Not as good:
public List<int> GetMeSomeInts() {...};
请记住,上述情况适用于您需要 IList
提供的功能,否则,返回 IEnumerable
。
看,爸爸,我在飞…
所以,您正在编码… 您决定想要一堆玩具,并对它们做些什么。您可能会本能地写
List<Toy> toyList = GetToyList();
Toy expensive = GetMostExpensiveToy(toyList);
其中方法看起来像
public Toy GetMostExpensiveToy(List<Toy> aCollection)
{
return aCollection.OrderBy(toy => toy.Price).Last();
}
恭喜您,您刚才承诺您的孩子会得到一套全套的鼓。
让我们快速分析一下这里发生了什么
- 您有一个项目的集合
- 您将它们传递给一个只遍历项目以查找某些内容的方法
- 您返回一个对象
如果有人尝试使用 Array
调用您的方法…它将失败。Collection
…失败…IEnumerable
…您猜对了…再次失败…您把自己锁在了只能传递 List
,即使您并不真正需要它。如果您使用了 List 实际需要的方法(IndexOf
、Insert
、RemoveAt
),那又是另一回事了,但在这种情况下,它是多余的。
这种编码方式的一个副作用是,您必须在您拥有的不同对象上调用 ToList()
或类似的操作,因为您要调用的方法接受 List<something>
,而您却拥有一个 Array<something>
。
更好的方法是这样编写方法
public Toy GetMostExpensiveToy(IEnumerable<Toy> aCollection)
{
return aCollection.OrderBy(toy => toy.Price).Last();
}
现在您可以使用数组、集合、列表或任何实现 IEnumerable 的对象来调用它,它都会正常工作。
返回值也是同样的逻辑。如果您不需要特定类型,请返回一个更广泛的类型(例如,而不是返回一个列表,返回一个 IEnumerable。这完全取决于您实际上要如何处理该集合。
牢记这一点将有助于您实际使用您需要的东西,而无需为不需要的功能支付额外费用,同时还能保持您的设计和代码的灵活性,并减少因更改而导致的破坏。
额外学分
我强烈建议您阅读 MSDN 集合指南。它将为您阐明集合的“是”与“否”方面的更精细之处。
返回空集合
在某些情况下,您的方法将不得不返回一个空集合。如果您要为自己的孩子购买玩具,您只会关注属于 NoiseLevel.Quiet
的玩具。假设您正在为一家在线商店实现一个方法,该方法可以接受玩具的噪音级别作为参数,以便人们可以只搜索 NoiseLevel.Quiet
。
如果您正在处理 List<Toy>
,则很容易返回一个空列表。只需编写 return new List<Toy>();
即可。
但是,如果您要尝试返回一个空的 IEnumerable<Toy>
呢?
我想到了两种选择。
- 您可以写
return Enumerable.Empty<Toy>();
- 您可以使用
yield break;
IEnumerable<Toy> GetQuietToys(NoiseLevel aNoiseLevel) {
var matching_products = CallDbHere();
// Let's assume that there are no matching products, we'll
// return an empty IEnumerable<Toy> using yield:
yield break;
// The above is the same as :
//
// return Enumerable.Empty<Toy>();
//
}
代码注释
在附加的项目中,您可以看到关于 Array
、Collection
和 List
不同用法的示例,与虚拟的 Toy
类有关。
在“Get Collection methods”区域,您会注意到每个方法都返回其名称所暗示的特定类型。这样做是为了创建特定的集合变量,并展示何时/为何您要从方法中返回特定的集合。
在“Get Toy methods”部分,您会注意到所有参数的类型都是 IEnumerable<Toy>,
,因此您实际上可以使用这三个集合变量来调用它们,它们仍然可以工作。
这是输出的屏幕截图
总结
如果您觉得这很有用,请随时投票、添加书签和/或留言。
我希望从现在开始,您再也不会向孩子们承诺鼓了;)
历史
- 2014 年 10 月 26 日:添加了屏幕截图、示例和关于泛型的说明。
- 2014 年 10 月 21 日:一些拼写错误,并编辑了一些段落以提高可读性/添加了示例。
- 2014 年 10 月 16 日:初次发布。