# 六、使用集合和泛型

您已经在前面的章节中看到了数组，它们在需要固定大小、强类型对象列表的场景中非常有用。但是，很多时候需要将对象组织成不同类型的数据结构，如列表、队列、栈和字典。控件中的集合类，C# 开发人员可以使用这些功能。NET 框架。

使用集合的核心部分是泛型的使用，它允许您使用参数化代码。这允许您强有力地键入您的收藏。您甚至可以编写自己的使用泛型的代码，允许您创建强类型的可重用库。

| ![](img/note.png) | 注意:的第一个版本。NET 提供了一个基于非强类型对象的集合库。毕竟。NET 类型可分配给对象，这起作用了。但是，您必须编写大量代码，使用强制转换运算符将对象转换回添加到集合中的类型。泛型解决了这个问题，使用泛型集合是。NET。 |

## 使用集合

。NET 集合类允许您以多种不同的方式处理数据。可以用`List`代替数组。如果需要一套先进先出的物品，可以使用`Queue`。如果你需要处理具有唯一标识的物品，你可以使用`Dictionary`。使用泛型，您可以构建自己的集合，以任何需要的方式管理数据。

| ![](img/tip.png) | 提示:查看系统。集合。在编写自己的集合之前使用通用命名空间；你可能会发现你需要的东西已经写好了。 |

一个常见的集合是`List`，这是一个数组的很好的替代。下面的清单提供了一个例子。

```cs
    using System;
    using System.Collections.Generic;

    public class Company
    {
        public string Name { get; set; }
    }

    public class Program
    {
        public static void Main()
        {
            List<string> names = new List<string>();
            names.Add("Joe");
            names.Insert(0, "Car");
            names.Add("Jill");
            names[0] = "Building";
            names.RemoveAt(0);
            Console.WriteLine($"First name: {names[0]}");

            IList<Company> companies = new List<Company>
            {
                new Company { Name = "Syncfusion" },
                new Company { Name = "Microsoft" },
                new Company { Name = "Acme" }
            };

            foreach (Company cmp in companies)
                Console.WriteLine(cmp.Name);

            Console.ReadKey();
        }
    }

```

代码清单 85

之前的程序展示了集合的多功能性。你有一个`string`的`List`，它只保存`string`类型的物体。这是一个泛型集合，这意味着它的类型参数在`<`和`>`中指定该集合使用的对象类型。添加的每一项都会附加到列表中，列表会动态增长。`Insert`操作在列表的第一个位置添加一个新的`string`，并将第一个`"Joe"`下推到索引 1 的第二个位置。第二个`Add`将`"Jill"`置于指数 2。请注意如何使用索引器(类似数组)语法来访问列表元素。`RemoveAt`删除了集合第一个索引处的字符串，将`"Joe"`移动到 0，`"Jill"`移动到 1。

`Main`中的第二个`List`展示了如何使用自定义类型。由于`List`是从`IList`派生的，所以您可以将实例分配给该接口。这很方便，因为这意味着你可以创建在`IList`上运行的代码；无论调用者传入的是`List<T>`还是从`IList`派生的任何其他集合类型，您的代码都仍然可以工作。

该示例还使用集合初始化语法，您可以实例化填充`List`的集合类型的逗号分隔列表。`foreach`语句遍历集合，打印每个项目。

前面的例子使用了`foreach`循环，但是你也可以使用`List`的`ForEach`方法，如下例所示。

```cs
            List<Company> companyList = companies as List<Company>;
            companyList.ForEach(cmp => Console.WriteLine(cmp.Name));

```

代码清单 86

第一行使用`as`运算符将`IList<Company>`转换为`List<Company>`。有了`List<T>`的实例，你可以调用`ForEach`方法，它接受一个λ参数。这个λ对`List<T>`中的每个项目执行，λ参数`cmp`包含当前项目。

这应该会让你了解`List`是如何工作的。通过阅读`List`课程的文档，您可以了解更多可用的方法。

另一个有用的收藏是`Dictionary`。它的工作方式类似于哈希表，您可以通过索引存储和检索对象，如下例所示。

```cs
    using System;
    using System.Collections.Generic;

    public class Customer
    {
        public int ID { get; set; }
        public string Name { get; set; }
    }

    public class Program
    {
        public static void Main()
        {
            Dictionary<int, Customer> customers = new Dictionary<int, Customer>();
            Customer jane = new Customer { ID = 0, Name = "Jane" };
            Customer joe = new Customer { ID = 1, Name = "Joe" };
            customers.Add(jane.ID, jane);
            customers[joe.ID] = joe;

            foreach (int key in customers.Keys)
                Console.WriteLine(customers[key].Name);

            Dictionary<int, Customer> customers2 = new Dictionary<int, Customer>
            {
                [0] = new Customer { ID = 0, Name = "Chris" },
                [1] = new Customer { ID = 1, Name = "Alex" }
            };

            Console.ReadKey();
        }
    }

```

代码清单 87

上例中的`Dictionary`分别为键和值取了两个类型参数。第一个示例实例化字典以获取一个`int`键和`Customer`值。`Customer`类有两个属性，其中`ID`将用作字典的关键字。请注意，您可以通过`Add`方法或索引器分配向字典添加值的两种不同方式。`Add`的第一个参数是索引，第二个参数是值。使用索引器时，将索引放在括号中并赋值。就像在其他集合中一样，有许多可用的方法，您应该查看该集合的文档。

`foreach`循环展示了如何迭代`Dictionary`项。一个`Dictionary`有一个`Keys`属性，它是键的集合，还有一个`Values`属性，它是值的集合(前面例子中的`Customer`实例)。注意循环如何使用索引器`customers[key]`访问与每个键相关的值。

示例中的第二个`Dictionary`展示了如何使用字典初始化器语法。只需将该值分配给与该值的键匹配的索引。

## 编写通用代码

泛型的主要应用之一是支持集合。在前一节中，您看到了如何使用集合。你也可以写自己的收藏类。如果您编写了一个通用链表，您将需要一个`Node`类来保存一个对象并引用列表中的下一个对象，还需要一个`LinkedList`集合类来执行列表操作。下面清单中的`Node`类包含一个对象实例。

```cs
    class Node<T>
    {
        public T Item { get; set; }
        public Node<T> Next;

        public Node(T item)
        {
            Item = item;
        }
    }

```

代码清单 88

`<T>`语法使`Node<T>`类通用。每当代码实例化一个`Node`，它就指定一个替换`T`的类型。在任何使用该类型对象的地方，指定`T`。`Node<T>`没有访问修饰符，因为它只用于这个程序集中的代码，默认的内部可访问性是合适的。以下示例显示了如何实例化`Node<T>`。

```cs
    Node<string> name = new Node<string>("May");

```

代码清单 89

在这里，你看到的`Node<string>`是类型，意思是你看到的`Node`类里面的所有地方都是现在的`string`。你不会将`int`、`decimal`或任何其他类型传递给这个类的构造函数，因为它只会持有`string`。它是强类型的。

接下来，您需要一个集合，将`Node<T>`实例保存为一个链表，如下面的列表所示。

```cs
    Using System;
    using System.Collections;
    using System.Collections.Generic;

    public class LinkedList<T> : IList<T>
    {
        Node<T> head;
        Node<T> tail;

        public void Add(T item)
        {
            var node = new Node<T>(item);

            if (head == null)
                head = node;
            else
                tail.Next = node;

            tail = node;
        }

        // Other IList members...
    }

```

代码清单 90

`LinkedList`类是通用的，并保存它被实例化为的类型的项目。`IList<T>`界面属于 FCL，便于创建收藏。正如您对接口的期望，为`IList`接口编写代码的开发人员也可以使用这个集合。`LinkedList`类实现了`IList<T>`接口的所有成员，这是必须的。

`Add`是一个最小的实现，但是说明了使用泛型的一些概念。即使代码实例化了一个新的`Node<T>`，实际类型将与`LinkedList`定义的类型相同。同样的概念也适用于`IList<T>`变成与`LinkedList`相同类型的界面。以下示例实例化了一个`LinkedList<T>`。

```cs
    public class Program
    {
        public static void Main()
        {
            var llist = new LinkedList<string>();
            llist.Add("Jamie");
            llist.Add("Ron");
            //...

            Node<string> name = new Node<string>("May");
        }
    }

```

代码清单 91

这表明您像任何其他集合一样实例化和使用您的泛型集合。只需在实例化过程中提供类型，集合就可以处理该类型的对象。

您在任何地方看到正在使用的`object`类型都是创建泛型类型的潜在候选。所有类型都继承了`object`类型，这就是为什么您会看到 FCL 和其他地方的类型使用对象类型值。

您也可以创建泛型方法。以下示例显示了几个工厂方法，其中一个是类型`object`，另一个是泛型。

```cs
    using System;

    public class CustomerReport
    {
        public DateTime Date { get; set; }
    }

    public class OrdersReport
    {
        public DateTime Date { get; set; }
    }

    public class ReportFactory
    {
        public static object Create(Type reportType)
        {
            switch (reportType.ToString())
            {
                case "CustomerReport":
                    var custRpt = new CustomerReport();
                    custRpt.Date = DateTime.Now;
                    return custRpt;
                default:
                case "OrdersReport":
                    var ordsRpt = new OrdersReport();
                    ordsRpt.Date = DateTime.Now;
                    return ordsRpt;
            }
        }
    }

    public class Program
    {
        public static void Main()
        {
            var rpt = (CustomerReport)ReportFactory.Create(typeof(CustomerReport));
            Console.ReadKey();
        }
    }

```

代码清单 92

您应该从之前的`ReportFactory`实现中得到的是，代码中有很多重复，并且在`Main`方法中使用了强制转换和`typeof`操作符，包括了比必要更多的语法。您可能会看到这些代码变得越来越复杂，越来越难以维护。以下示例显示了如何将`Create`方法重构为泛型方法。

```cs
    using System;

    public abstract class Report { }

    public class CustomerReport : Report
    {
        public DateTime Date { get; set; }
    }

    public class OrdersReport : Report
    {
        public DateTime Date { get; set; }
    }

    public class ReportFactory
    {
        public static TReport Create<TReport>()
            where TReport : Report
        {
            switch (typeof(TReport).Name)
            {
                case "CustomerReport":
                    var custRpt = new CustomerReport();
                    custRpt.Date = DateTime.Now;
                    return (TReport)(Report)custRpt;
                default:
                case "OrdersReport":
                    var ordsRpt = new OrdersReport();
                    ordsRpt.Date = DateTime.Now;
                    return (TReport)(Report)ordsRpt;
            }
        }
    }

    public class Program
    {
        public static void Main()
        {
            var rpt2 = ReportFactory.Create<CustomerReport>();

            Console.ReadKey();
        }
    }

```

代码清单 93

`Create`方法有一个新的类型参数`TReport`。在前面的例子中，您已经看到了仅使用`T`的情况，但是有时候——就像在`Dictionary<TKey`和`TValue>`中一样——您必须区分多个类型参数，或者使代码更加自文档化。返回类型现在也是强类型的。代码能够从派生类型转换到`Report`，然后转换到`TReport`以返回正确的类型。这是允许的，因为通用约束条件`TReport : Report`规定`TReport`必须从`Report`派生。调用代码要简单得多。

`Create<TReport>`方法仍然比它必须的要长，并且包含了太多的重复。我们可以用一般的约束来解决这个问题。约束顾名思义就是:它限制了类型的泛型程度。您在前面的代码中看到了对`Report`的基类约束。下表描述了所有可用的约束。

表 3:泛型类型约束

| 限制 | 描述 |
| --- | --- |
| 连接 | 类型必须实现指定的接口。 |
| 基本类 | 类型必须从指定的基类派生。 |
| 班级 | 类型必须是引用类型。 |
| 结构体 | 类型必须是值类型。 |
| 新的 | 类型必须有默认(无参数)构造函数。 |

我们需要两个约束来简化我们的代码:`interface`和`new`。以下示例显示了如何使用它们。

```cs
    using System;

    public interface IReport
    {
        DateTime Date { get; set; }
    }

    public class CustomerReport : IReport
    {
        public DateTime Date { get; set; }
    }

    public class OrdersReport : Report, IReport
    {
        public DateTime Date { get; set; }
    }

    public class ReportFactory
    {
        public static TReport Create<TReport>()
            where TReport : IReport, new()
        {
            return new TReport() { Date = DateTime.Now };
        }
    }

    public class Program
    {
        public static void Main()
        {
            var rpt2 = ReportFactory.Create<CustomerReport>();

            Console.ReadKey();
        }
    }

```

代码清单 94

在这个演示中，有一个新的界面`IReport`，它是`CustomerReport`和`OrdersReport`派生出来的。因为我们知道我们期望的类是`IReport`，我们可以对类型做出假设，并编写在任何`IReport`上运行的代码。

`Create<TReport>`方法在方法签名后有额外的语法。要指定一个约束，在要约束的类型后面跟随`where`关键字，附加一个分号，然后从上一个表中添加一个逗号分隔的约束列表。这个例子使用了一个接口和`new()`约束。`new()`约束意味着我们可以创建一个新的类型实例，`new TReport()`。此外，由于类型是一个`IReport`，我们知道它有一个`Date`属性，可以填充它的`Date`属性。重复和过多的代码已经不复存在，在实现和使用上都被通用代码简化了。

| ![](img/tip.png) | 提示:您也可以创建通用委托。像往常一样，您应该寻求重用已经存在于 FCL 的类型。中流行的可重用委托。NET 框架是事件处理程序<teventargs>。事实上，您可以将[第 5 章](5.html#_Chapter_5_)中对 ClickHandler 的所有引用替换为事件处理程序< ClickEventArgs >，您的代码仍然可以工作。</teventargs> |

## 总结

您已经看到了如何使用泛型，并且它们允许您编写可重用的代码。那个。NET 集合类比数组更通用，允许您以更有助于应用程序设计的方式管理对象。