您已经在前面的章节中看到了数组,它们在需要固定大小、强类型对象列表的场景中非常有用。但是,很多时候需要将对象组织成不同类型的数据结构,如列表、队列、栈和字典。控件中的集合类,C# 开发人员可以使用这些功能。NET 框架。
使用集合的核心部分是泛型的使用,它允许您使用参数化代码。这允许您强有力地键入您的收藏。您甚至可以编写自己的使用泛型的代码,允许您创建强类型的可重用库。
| | 注意:的第一个版本。NET 提供了一个基于非强类型对象的集合库。毕竟。NET 类型可分配给对象,这起作用了。但是,您必须编写大量代码,使用强制转换运算符将对象转换回添加到集合中的类型。泛型解决了这个问题,使用泛型集合是。NET。 |
。NET 集合类允许您以多种不同的方式处理数据。可以用List
代替数组。如果需要一套先进先出的物品,可以使用Queue
。如果你需要处理具有唯一标识的物品,你可以使用Dictionary
。使用泛型,您可以构建自己的集合,以任何需要的方式管理数据。
| | 提示:查看系统。集合。在编写自己的集合之前使用通用命名空间;你可能会发现你需要的东西已经写好了。 |
一个常见的集合是List
,这是一个数组的很好的替代。下面的清单提供了一个例子。
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
方法,如下例所示。
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
。它的工作方式类似于哈希表,您可以通过索引存储和检索对象,如下例所示。
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
类包含一个对象实例。
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>
。
Node<string> name = new Node<string>("May");
代码清单 89
在这里,你看到的Node<string>
是类型,意思是你看到的Node
类里面的所有地方都是现在的string
。你不会将int
、decimal
或任何其他类型传递给这个类的构造函数,因为它只会持有string
。它是强类型的。
接下来,您需要一个集合,将Node<T>
实例保存为一个链表,如下面的列表所示。
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>
。
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
,另一个是泛型。
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
方法重构为泛型方法。
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
。以下示例显示了如何使用它们。
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
属性。重复和过多的代码已经不复存在,在实现和使用上都被通用代码简化了。
| | 提示:您也可以创建通用委托。像往常一样,您应该寻求重用已经存在于 FCL 的类型。中流行的可重用委托。NET 框架是事件处理程序。事实上,您可以将第 5 章中对 ClickHandler 的所有引用替换为事件处理程序< ClickEventArgs >,您的代码仍然可以工作。 |
您已经看到了如何使用泛型,并且它们允许您编写可重用的代码。那个。NET 集合类比数组更通用,允许您以更有助于应用程序设计的方式管理对象。