您创建的任何应用程序。网络平台将需要与维护和维护的问题进行斗争操作内存中的一组数据点。这些数据点可以来自任何不同的位置包括关系数据库、本地文本文件、XML文档、web服务调用,或者可能用户提供的输入。
当。NET平台首次发布,程序员经常使用系统的类。集合名称空间存储和与应用程序中使用的数据进行交互。进去。NET 2.0,c#编程语言被增强支持一个被称为泛型的特性;随着这种变化,在基类库中引入了一个新的名称空间:system . collections.common。
本章将概述. net基类库中的各种集合(泛型和非泛型)名称空间和类型。您将看到,与非泛型容器相比,泛型容器通常更受欢迎,因为它们通常提供更大的类型安全性和性能优势。在您学习了如何创建和操作框架中找到的泛型项之后,本章的其余部分将研究如何构建您自己的泛型方法和泛型类型。当您这样做时,您将了解约束(以及相应的c# where关键字)的作用,它允许您构建非常类型安全的类。
可以用来保存应用程序数据的最原始容器无疑是数组。正如您在第4章中看到的,c#数组允许您定义一组相同类型的项(包括一个系统数组)。对象,它本质上表示任意类型数据的数组)的固定上限。还记得在第4章中,所有c#数组变量都从系统中收集了大量的功能。数组类。通过快速回顾,考虑下面的Main()方法,它创建了一个文本数据数组,并以各种方式操纵其内容
static void Main(string[] args) { //创建一个子串数组 string[] strArr = { "First", "Second", "Third" }; //显示子串数组的长度 Console.WriteLine("本数组有{0}项", strArr.Length); Console.WriteLine(); //Display contents using enumerator. foreach (string item in strArr) { Console.WriteLine("数组项内容:{0}", item); } Console.WriteLine(); //Reverse the array and print again. Array.Reverse(strArr); foreach (var item in strArr) { Console.WriteLine("数组项内容:{0}", item); } Console.ReadKey(); } |
虽然基本数组可以是有用的管理少量固定大小的数据,还有许多其他的时间,你需要一个更灵活的数据结构,如动态增长和萎缩的容器或集装箱可容纳的对象只能满足一个特定的标准(例如,只有对象源于特定的基类或对象实现特定的接口)。当您使用简单数组时,请始终记住它们的大小是固定的。如果你创建一个由三个元素组成的数组,你只会得到三个元素;因此,以下代码将导致运行时异常(确切地说,是IndexOutOfRangeException)
注意,实际上可以使用通用的Resize()<T>方法更改数组的大小。然而,这将导致将数据复制到新的数组对象中,并且可能效率低下。
为了帮助克服简单数组的限制,. net基类库附带了许多包含集合类的名称空间。与简单的c#数组不同,集合类是在您插入或删除项时动态调整大小的。此外,许多集合类提供了更高的类型安全性,并且高度优化以内存有效的方式处理所包含的数据。当您阅读本章时,您将很快注意到collection类可以属于两个大类中的一个。
非泛型集合(System.Collections namespace)
泛型集合(System.Collections.Generic)
非泛型集合通常设计用于在系统上操作。因此,对象类型是松散类型的容器(然而,一些非泛型集合只对特定类型的数据进行操作,比如string对象)。相比之下,泛型集合的类型安全得多,因为必须在创建泛型集合时指定泛型集合所包含的类型。您将看到,任何泛型项的标识符都是用尖括号标记的类型参数(例如List<T>)。您将在本章的稍后部分研究泛型的细节(包括它们提供的许多好处)。现在,让我们研究一下系统中的一些关键非泛型集合类型。和System.Collections集合。专业名称空间。
当. net平台首次发布时,程序员经常使用系统中发现的非泛型集合类。集合名称空间,其中包含一组用于管理和组织大量内存数据的类。表9-1说明了这个名称空间中一些比较常用的集合类及其实现的核心接口。
表9-1有用的系统集合类型
System.Collections 类 | 意义 | 重要实现接口 |
ArrayList[数组列表] | 表示按顺序列出的对象的动态大小集合 | IList,ICollection, IEnumerable,ICloneable |
BitArray[比特数组] | 管理一个紧凑的比特值,这代表了布尔 | IEnumerable,ICloneable |
HashTable[哈希表] | 表示基于键的基于哈希码的键-值对的集合 | IEnumerable,ICloneable |
Queue[队列] | 表示一个标准的先入先出(FIFO)集合 | IEnumerable,ICloneable |
SortedList[排序列表] | 表示按键排序并可按键和索引访问的键-值对的集合 | IDictionary, ICollection, IEnumerable,ICloneable |
Stack[栈] | 后入先出(LIFO)栈提供了push,pop方法 | ICollection, IEnumerable,ICloneable |
这些集合类实现的接口提供了对其总体功能的深入了解。表9-2记录了这些关键接口的总体性质.
表9-2 由System.Collections支持的关键接口
System.Collections 类 | 意义 |
ICollection | 为所有非泛型集合类型定义一般特征(例如,大小、枚举和线程安全性) |
IConeable | 允许实现对象向调用者返回自身的副本 |
IDictionary | 允许非泛型集合对象使用键值对表示其内容 |
IEnumerable | 返回一个实现IEnumerator接口的对象(参见下一个表项) |
IEnumerator | 启用集合项的foreach样式的迭代(枚举器) |
IList | 提供在顺序对象列表中添加、删除和索引项的行为 |
举例:使用ArrayList
根据您的经验,您可能有一些使用(或实现)这些经典数据结构(如堆栈、队列和列表)的第一手经验。如果不是这样,那么在本章稍后的部分中,当您研究它们的一般对应项时,我将提供关于它们之间差异的一些详细信息。在此之前,这里有一个使用ArrayList对象的Main()方法。注意,您可以动态添加(或删除)项,容器会自动调整大小。
static void Main(string[] args) { ArrayList strArray = new ArrayList(); //使用AddRange 添加数组 strArray.AddRange(new string[] { "First", "Second", "Third" }); //获取ArrayList容量 Console.WriteLine("这个集合一共有{0}项", strArray.Count); Console.WriteLine(); //动态扩容 strArray.Add("Fourth"); Console.WriteLine("这个集合一共有{0}项", strArray.Count); Console.WriteLine(); //遍历内容 foreach (var item in strArray) { Console.WriteLine(item.ToString()); } Console.WriteLine(); Console.ReadLine(); } |
System.Collections不是唯一包含非泛型集合类的。net名称空间。System.Collections。专门化名称空间定义了许多(请原谅冗余)专门化集合类型。表9-3说明了这个特定的以集合为中心的名称空间中一些更有用的类型,它们都是非泛型的。
System.Collections.Specialized | 意义 |
HybridDictionary[混合字典] | 这个类在集合小的时候使用ListDictionary实现IDictionary,然后在集合大的时候切换到Hashtable。 |
ListDictionary[列表字典 | 当您需要管理少量随时间变化的项目(大约10个)时,这个类非常有用。该类使用单链表来维护其数据。 |
StringCollection[字符串集合] | 该类提供了管理大量字符串数据集合的最佳方法。 |
BitVector32[位向量32 | 该类提供一个简单的结构,在32位内存中存储布尔值和小整数。 |
除了这些具体的类类型之外,这个名称空间还包含许多额外的接口和抽象基类,您可以将它们用作创建自定义集合类的起点。虽然在某些情况下,这些专门化类型可能正是您的项目所需要的,但是在这里我不会评论它们的用法。同样,在很多情况下,您可能会发现System.Collections。泛型名称空间提供具有类似功能和额外好处的类。
注意,有两个额外的集合中心名称空间(system . collection)。ObjectModel和系统.并发)在其中。net基类库。您将检查以前的名称空间在这一章之后,在你对泛型的话题感到满意之后。系统. .并发提供适合于多线程环境的集合类(参见第19章的信息多线程)。
虽然许多成功的。net应用程序都是多年来使用这些非泛型集合类(和接口)构建的,但历史表明使用这些类型会导致许多问题。
第一个问题是使用这个系统。和System.Collections集合。专门化类可能导致一些性能很差的代码,特别是在处理数值数据(例如值类型)时。稍后您将看到,当您将结构存储在任何非泛型集合类原型中以对系统进行操作时,CLR必须执行许多内存传输操作。对象,这可能会降低运行时执行速度。
第二个问题是,大多数非泛型集合类都不是类型安全的,因为(同样)它们是为在系统上操作而开发的。对象,因此它们可以包含任何东西。如果. net开发人员需要创建一个高度类型安全的集合(例如,一个容器可以容纳只实现特定接口的对象),那么惟一的实际选择就是手工创建一个新的集合类。这样做并不需要太多的劳动,但是有点乏味。
在查看如何在程序中使用泛型之前,您会发现更仔细地研究非泛型集合类的问题很有帮助;这将帮助您更好地理解泛型首先要解决的问题。如果您想继续,请创建一个名为IssuesWithNonGenericCollections的新控制台应用程序项目。接下来,确保导入系统。集合命名空间到c#代码文件的顶部。
您可能还记得第4章,.NET平台支持两大类数据:值类型和引用类型。鉴于.NET定义了两大类类型,您可能偶尔需要将一个类别的变量表示为另一个类别的变量。为此,c#提供了一个简单的方法一种称为装箱的机制,用于将数据存储在引用变量中的值类型中。假设您在一个名为SimpleBoxUnboxOperation()的方法中创建了一个int类型的局部变量。如果,在在您的应用程序过程中,您要将此值类型表示为引用类型,您要将值,如下:
static void SimpleBoxUnBoxOperation() { //创建一个值类型 int myInt = 25; //把值类型装箱 object boxedInt = myInt; } |
装箱可以正式定义为显式地为系统分配值类型的过程。对象变量。当您将一个值装箱时,CLR会在堆上分配一个新对象,并将值类型s值(本例中为25)复制到该实例中。返回给您的是对新分配的基于堆的对象的引用。
相反的操作也可以通过解装箱来实现。的转换过程对象引用中保存的值返回到堆栈上对应的值类型。从语法上来说,拆箱操作看起来像普通的铸造操作。然而,语义是非常不同的。CLR首先验证接收数据类型是否与装箱类型等效,如果是,则将该值复制回基于本地堆栈的变量。例如,下面的开箱操作可以工作成功地,假定boxedInt的底层类型确实是一个int:
//拆箱
int unboxedInt =(int) boxedInt;
当c#编译器遇到装箱/拆箱语法时,它会发出包含box/unbox op代码的CIL代码。如果要使用ildasm检查编译后的程序集(反编译),你会发现下面这些
请记住,与执行典型的强制转换不同,必须将复选框解压为适当的数据类型。如果试图将一段数据解压为不正确的数据类型,将抛出InvalidCastException异常。为了完全安全,您应该在try/catch逻辑中封装每个拆箱操作;然而,对于每一个拆箱操作,这将是相当劳动密集型的。考虑下面的代码更新,它将抛出一个错误,因为您正试图将已装箱的int类型解压为long[会报错]
try { long unboxLong = (long)boxedInt; } catch (Exception ex) { Console.WriteLine(ex.Message); } |
乍一看,装箱/拆箱似乎是一种相当平淡无奇的语言特性,更多的是学术性而非实用性。毕竟,很少需要在局部对象变量中存储局部值类型,如下所示。然而,事实证明装箱/拆箱过程非常有用,因为它允许您假设所有东西都可以作为一个系统来处理。对象,而CLR则代表您处理与内存相关的详细信息。
让我们看看这些技术的实际应用。假设您已经创建了一个非泛型系统。集合。保存一批数字(堆栈分配的)数据。如果你要检查ArrayList的成员,您会发现它们是用于在system.object
的原型。对象数据。现在考虑Add()、Insert()和Remove()方法以及类索引器。
ArrayList已经建立在对象上,它表示在堆上分配的数据,因此看起来很奇怪,下面的代码编译并在不抛出错误的情况下执行:
ArrayList myInts = new ArrayList();
myInts.Add(10);
myInts.Add(20);
myInts.Add(30);
上面的Add方法会自动把值类型装箱为object. 尽管您将数值数据直接传递到需要对象的方法中,但是运行时将自动代表您对基于堆栈的数据进行装箱。稍后,如果希望使用类型索引器从ArrayList检索项,必须使用强制转换操作将堆分配的对象解压为堆栈分配的整数。记住,ArrayList的索引器正在返回系统是对象,而不是System.Int32s。
foreach (object item in myInts)
{
//Console.WriteLine("该项乘以2等于:{0}", item*2);//报错,徐拆箱后使用
Console.WriteLine("该项乘以2等于:{0}", (int)item * 2);
}
同样,请注意堆栈分配的系统。Int32在调用ArrayList.Add()之前被装箱,因此它可以在所需的System.Object中传递。还要注意系统。对象被拆回系统中。Int32一旦通过强制转换操作从ArrayList中检索到它,当它被传递给Console.WriteLine()方法时,只需要再次装箱,因为该方法在系统上运行。对象变量。
从程序员的角度来看,装箱和解箱很方便,但是这种简化的堆栈/堆内存传输方法带来了性能问题(在执行速度和代码大小方面)和缺乏类型安全性的负担。要理解性能问题,请考虑box和unbox一个简单整数必须执行的这些步骤:
1. 必须在托管堆上分配一个新对象。
2. 基于堆栈的数据的值必须传输到该内存位置。
3. 解箱时,必须将存储在基于堆的对象上的值传输回堆栈。
4. 堆上现在未使用的对象(最终)将被垃圾收集。
虽然这种特殊的WorkWithArrayList()方法不会造成性能方面的主要瓶颈,但是如果一个ArrayList包含数千个整数,而您的程序在一定程度上定期对这些整数进行操作,那么您肯定会感受到这种影响。在理想的情况下,您可以在容器中操作基于堆栈的数据,而不会出现任何性能问题。理想情况下,如果不需要使用try/catch作用域从这个容器中提取数据就好了(这正是泛型所允许的)。
在介绍开箱操作时,我提到了类型安全问题。回想一下,您必须将数据解压到与装箱之前声明的数据类型相同的数据类型中。然而,在无泛型的世界中,您必须记住类型安全的另一个方面:系统的大多数类。集合通常可以包含任何内容,因为它们的成员是原型化的,可以在system . object上操作。例如,该方法构建一个由不相关数据的随机位组成的ArrayList
ArrayList allMyObjects = new ArrayList(); //创建一个非泛型集合对象,可以添加各种对象 allMyObjects.Add(true); allMyObjects.Add(new OperatingSystem(PlatformID.MacOSX, new Version(10, 0))); allMyObjects.Add(66); allMyObjects.Add(3.140); |
在某些情况下,您将需要一个非常灵活的容器,它可以容纳任何东西(如图所示)。但是,大多数情况下,您需要一个类型安全的容器,它只能对特定类型的数据点进行操作。例如,您可能需要一个容器,它只能容纳数据库连接、位图或与ipoint兼容的对象。
在泛型之前,解决这个类型安全问题的唯一方法是手动创建一个自定义(强类型)集合类。假设您想创建一个自定义集合,该集合只能包含Person类型的对象。
public class Person { public int Age { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public Person() { } public Person(string firstName, string lastName, int age) { Age = age; FirstName = firstName; LastName = lastName; } public override string ToString() { return $"Name:{FirstName} {LastName},Age:{Age}"; } } |
要构建只能容纳Person对象的集合,可以定义System.Collections。类中的ArrayList成员变量,并将所有成员配置为操作强类型的Person对象,而不是系统。对象类型。下面是一个简单的例子
(一个产品级自定义集合可以支持许多额外的成员,并可能扩展一个从系统中抽象的基类。集合或系统。专业名称空间:
public class PersonCollection : IEnumerable { private ArrayList arPeople = new ArrayList(); public Person GetPerson(int pos) => (Person)arPeople[pos]; public void AddPerson(Person p) { arPeople.Add(p); } public void ClearPeople() { arPeople.Clear(); } public int Count => arPeople.Count; IEnumerator IEnumerable.GetEnumerator() => arPeople.GetEnumerator(); } |
注意,PersonCollection类实现了IEnumerable接口,它允许对每个包含的项进行类似foreachlike的迭代。还要注意,GetPerson()和AddPerson()方法已经被原型化为只对Person对象操作,而不是位图、字符串、数据库连接或其他项。定义了这些类型之后,就可以确保类型安全了,因为c#编译器能够确定任何插入不兼容数据类型的尝试。
static void UsePersonCollection()
{
Console.WriteLine("********自定义一个人类集合***********\n");
PersonCollection myPeople = new PersonCollection();
myPeople.AddPerson(new Person("Homer", "Simpson", 40));
myPeople.AddPerson(new Person("May", "Simpson", 38));
myPeople.AddPerson(new Person("Lisa", "Simpson", 9));
myPeople.AddPerson(new Person("Bart", "Simpson",7));
myPeople.AddPerson(new Person("Maggie", "Simpson",2));
//这时myPersonColletion中只能加入人类,加入其他类就报错
// myPeople.AddPerson(20);
//枚举器的实现
foreach (Person item in myPeople)
{
Console.WriteLine(item);
}
Console.ReadKey();
}
虽然自定义集合确实确保了类型安全性,但是这种方法使您必须为希望包含的每个惟一数据类型创建一个(几乎相同的)自定义集合(产生冗余)。因此,如果您需要一个只能对Car基类派生的类进行操作的自定义集合,那么您需要构建一个高度类似的集合类。
当您使用泛型集合类时,您将纠正前面的所有问题,包括装箱/取消装箱处罚和缺乏类型安全性。此外,构建自定义(通用)集合类的需求变得非常少。您可以使用泛型集合类并指定类型的类型,而不必构建可以包含人员、汽车和整数的惟一类。
考虑下面的方法,该方法使用泛型列表< T >类(在系统中)名称空间)以强烈类型的方式包含各种类型的数据:
static void UseGenericList() { Console.WriteLine("********可以使用泛型来解决创建对象集合********\n"); List<Person> myPeople = new List<Person>(); myPeople.Add(new Person("Harry", "Lee", 30)); myPeople.Add(new Person("Ada", "wang", 31)); //自动实现了枚举器的继承 foreach (Person item in myPeople) { Console.WriteLine(item); } } |
第一个列表<T>对象只能包含Person对象。因此,您不需要执行从容器中提取项时强制转换,这使得这种方法更加类型安全。换句话说,没有隐式的装箱或开箱,如您在非泛型ArrayList中发现的那样。下面是一些通用的好处容器与非通用容器相比:
1. 泛型提供了更好的性能,因为它们在存储值类型时不会导致装箱或取消装箱。
2. 泛型是类型安全的,因为它们只能包含您指定的类型的类型。
3. 泛型大大减少了构建自定义集合类型的需要,因为在创建泛型容器时指定了类型的类型。
咨询电话
0371-68632068