65.9K
CodeProject 正在变化。 阅读更多。
Home

了解您的集合:从 IEnumerable 到 List 及以后

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (29投票s)

2014年10月17日

CPOL

11分钟阅读

viewsIcon

53800

downloadIcon

407

理清集合之间的混淆

引言

我曾多次看到方法参数和返回值不合理,并讨厌在某处更改集合类型导致我不得不在不同地方更改方法签名。 

我想写这篇文章很久了,以帮助揭开一些集合及其含义周围的神秘光环

注意:您可以通过单击此处直接跳转到末尾的比较图。

关于泛型的一点说明

泛型在 .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 属性(我指向哪里),它的方法是 MoveNextReset

因此,如果您正在处理任何实现了 IEnumerable 的对象(无论是泛型还是非泛型),您就知道您可以获取当前对象,并且您可以向前移动或从零开始。最简单有用的例子是使用 Enumerable.Range 方法

var one_to_ten = Enumerable.Range(1, 10);

这将返回一个 Int 类型的 IEnumerable,其中包含数字 1 到 10(注意:这是惰性的,如果您不使用它,它将不会被调用)。

一个(有点性别歧视的)例子是一个女人和她的鞋子。她不关心有多少双,只要她能遍历所有鞋子,每天挑下一双。因为我觉得这不好,所以男性对应的是一个男人和他的电子游戏。外面有成千上万的游戏,但只要他能玩下一个,他就很开心。

注意:在这个级别上,泛型版本和非泛型版本之间唯一的真正区别是,非泛型版本有 4 个扩展方法(AsParallelAsQueryableCast<TResult>OfType<TResult>)。泛型版本有更多(多到我在这里就不一一列举了)。请随意查看 MSDN 页面或您的 Intellisense。

您可以在官方 MSDN 页面上阅读更多内容:泛型版本,非泛型版本。

ICollection<T>

这是下一个级别。它实现了 IEnumerable<T>IEnumerable,并将添加以下属性:CountIsReadOnly

您可以与它一起使用的新方法是:AddClearContainsCopyToRemove

这比 Enumerator 更具体一些,您可以对其进行操作、添加、删除以及检查某个对象是否存在于该集合中。如果您不关心对象在集合中的位置,那么这就是您想要使用的。

例如:鸡蛋在蛋托里。烘焙时,您想知道总共有 6 个鸡蛋,并想将它们加入面团,但您不关心它们的实际位置或放入的顺序。

在我看来,ICollection(非泛型)接口不是很实用。它添加了CountIsSynchronizedSyncRoot 属性,以及仅有的 CopyTo() 方法,该方法允许您将集合复制到数组中。

您可以在 MSDN 上的官方页面上阅读更多内容:泛型版本,非泛型版本。

IList<T>

此接口同时实现了 IEnumerable<T>ICollection<T>,并将添加以下属性:Item

您可以使用的Buongiorno新方法是:IndexOfInsertRemoveAt

此接口让您对集合有更“真实”的感觉,您可以查看哪个对象在哪里,在特定索引处添加,以及按索引删除特定项。

例如:您正在和朋友打牌,您知道牌堆中的第五张牌是鬼牌。知道它在牌堆中还不够。如果您道德败坏,下次您给自己抽牌时,您将使用 RemoveAt(4)

非泛型接口同时实现了 IEnumerableICollection,并将添加以下属性:IsFixedSizeIsReadOnlyItem

方法方面,非泛型接口看起来与其泛型对应项(共 7 个新方法),但其中 4 个只是非泛型 ICollection 中未实现的方法。

您可以在 MSDN 上的官方页面上阅读更多内容:泛型版本和非泛型版本。

ISet<T>

此接口提供了实现集合(集合,具有唯一元素和特定操作)的方法。 HashSet<T>SortedSet<T> 集合实现了此接口。

这对于处理集合的数学问题很有用。请访问此 MSDN 页面了解更多信息,并请注意,此接口没有非泛型版本(即没有 ISet)。

我添加此内容是因为在比较屏幕截图中,我添加了与我们讨论过的其他集合的区别。

Enumerable<T>、Collection<T>、List<T>

这些是您通常会使用的集合类,它们只是实现了我们之前讨论过的接口。

数组

数组是一个类,它实现了:ICloneableIListICollectionIEnumerableIStructuralComparableIStructuralEquatable.

它不是 System.Collections 的一部分,并且其大小是固定的。数组可能是“集合”最古老的形式,但由于其实现方式在不同版本的 .Net 中有所不同,因此它们存在一些细微的问题。数组不是泛型的,但它们也有点泛型。关于为什么没有泛型数组的冗长(但有趣的)回答,请访问 stack overflow 上的这个问题

当处理特定数量的对象时,您通常会想要一个数组。例如,您只允许从图书馆借 5 本书。一个大小为 5 的 Book[] 是一个很好的表示方式。另一种选择是当您从方法或库中获取数组时。

根据 MSDN,当不需要 AddRemove 时,也是一些好情况。

您可以在MSDN上的官方页面上阅读更多内容,并务必查看数组用法指南

其余的集合

您可以在 System.Collections 命名空间页面上找到 .Net 中其余的集合类型。

可视化辅助和总结

每个属性/方法都将具有其定义所在父集合的颜色。例如,Item 属性是绿色的,因为它定义在 IList<T> 中,而在同一列中,Count 是蓝色的,因为它定义在 ICollection<T> 接口上。

这是泛型比较表

这是非泛型对应项

起飞

现在我们已经涵盖了基础知识,我们将回到“何时使用什么”的问题,并使用我们的类比。

如果您是父亲,而您的儿子不愿意打扫房间,您可能会想贿赂他,并为他做他本应做的奖励。哪种选择看起来更好?

  1. 如果你打扫房间,我会给你一个玩具。
  2. 如果你打扫房间,我会给你一套 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();
}

恭喜您,您刚才承诺您的孩子会得到一套全套的鼓。

让我们快速分析一下这里发生了什么

  1. 您有一个项目的集合
  2. 您将它们传递给一个只遍历项目以查找某些内容的方法
  3. 您返回一个对象

如果有人尝试使用 Array 调用您的方法…它将失败。Collection…失败…IEnumerable…您猜对了…再次失败…您把自己锁在了只能传递 List,即使您并不真正需要它。如果您使用了 List 实际需要的方法(IndexOfInsertRemoveAt),那又是另一回事了,但在这种情况下,它是多余的。
这种编码方式的一个副作用是,您必须在您拥有的不同对象上调用 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>();
    //
}

代码注释

在附加的项目中,您可以看到关于 ArrayCollectionList 不同用法的示例,与虚拟的 Toy 类有关。

在“Get Collection methods”区域,您会注意到每个方法都返回其名称所暗示的特定类型。这样做是为了创建特定的集合变量,并展示何时/为何您要从方法中返回特定的集合。

在“Get Toy methods”部分,您会注意到所有参数的类型都是 IEnumerable<Toy>, ,因此您实际上可以使用这三个集合变量来调用它们,它们仍然可以工作。

这是输出的屏幕截图

总结

如果您觉得这很有用,请随时投票、添加书签和/或留言。

我希望从现在开始,您再也不会向孩子们承诺鼓了;)

历史

  • 2014 年 10 月 26 日:添加了屏幕截图、示例和关于泛型的说明。
  • 2014 年 10 月 21 日:一些拼写错误,并编辑了一些段落以提高可读性/添加了示例。
  • 2014 年 10 月 16 日:初次发布。
© . All rights reserved.