总体要求
1、了解集合的概念,初步掌握.NET Framework中常用集合的使用方法。
2、理解索引器的概念,能区别索引器与属性,掌握索引器的定义与使用。
3、了解泛型的相关概念,初步掌握泛型接口,泛型类、泛型属性和泛型方法的使用。
相关知识点
1、熟悉类和数组的定义和使用
2、熟悉类的方法成员的定义与使用
学习重点
集合、索引器、泛型的定义与使用
学习难点
1、索引器的作用、定义与使用方法
2、泛型的概念和意义,泛型的定义和使用方法
6.1 集合
数组是一种非常有用的数据结构,但是数组也具有很多局限性,首先,数组元素的数据类型必须是相同的,其次,在创建数组时必须确定元素个数。数组一旦创建,其大小就是固定的。想调整其大小或者增加新元素都是比较困难的。特别是,当对象的个数未知,并且随时可能要添加和移除时,数组并不是使用最方便的数据结构。为此,C#提供了集合,通过它来管理数据将更为方便。本节将详细介绍集合的使用方法。
6.1.1 集合概述
集合是通过高度结构化的方式存储任意对象的类,它可以把一组类似的对象组合在一起。与无法动态调整大小的数组相比,集合不仅能随意调整大小,而且为存储或检索某个对象提供了更多的方法。例如,由于Object是所有数据类型的基类,因此任何类型的对象( 包括任何值类型或引用类型数据)都可被组合到一个Object类型的集合中,并通过C#的foreach语句来访问其中的每一个对象。当然,对于一个Object类型的集合来说,可能需要单独对各元素执行附加的处理,例如,装箱、拆箱或转换等。
.NET Framework提供的集合位于System.Collections命名空间,其操作功能都统一在该命名空间中的相关接口中定义,表6-1列出了其中的4个重要接口。
表 6-1 System.Collection命名空间中部分接口
接口 | 作用 |
IEnumerable | 可以迭代集合中的项 |
ICollection | (继承于IEnumberable)可以获取集合中项个数,并能把项复制到一个简单的数组类型中 |
IList | (继承于IEnumberable)提供了集合的项列表,并可以访问这些项,以及其他一些与项列表相关的功能 |
IDictionary | (继承于IEnumberable和ICollection)类似于IList,但提供了可通过键值而不是索引访问的项列表 |
通过System.Collections命名空间,可以在程序中直接使用由.NET Framework提供的集合类,也可以从这些接口派生出自己的集合类,以管理更复杂的数据。
.NET Framework提供的集合包括数组、列表、哈希表、字典、队列和堆栈等简单集合类型,还包括有序列表、双向链表和有序字典派生集合类型。表6-2列出了其中的10个常用集合类。
表 6-2 常用的集合类
集合 | 含义 |
Array | 数组 |
List | 列表 |
ArrayList | 动态数组 |
Hashtable | 哈希表 |
Dictionary | 字典(键/值对集合) |
Queue | 队列 |
Stack | 栈 |
SortedList | 有序键/值对列表 |
LinkedList | 双向链表 |
SortedDictionary | 有序字典 |
另外,.NET Framework也提供了一些专用集合用于处理特定的元素类型,包括StringCollection、StringDictionary和NameValueCollection等。其中,StringCollection是字符串集合,由若干个字符串组成。字符串集合与字符串数组的区别在于,字符串集合提供了大量的可直接调用的方法,包括Add(添加字符串)、Clear(清空集合)、Contains(是否包含特定字符串)、IndexOf(搜索特定字符串)、Insert(插入字符串)和Remove(移除特定字符串)等。
6.1.2 ArrayList
ArrayList是一个可动态维护长度的集合,又称动态数组,它不限制元素的个数和数据类型,允许把任意类型的数据保存到ArrayList中。数组类Array与动态数组类ArrayList的主要区别如下。
(1)Array的大小是固定的,而ArrayList的大小可根据需要自动扩充;
(2)在Array中一次只能读写一个元素的值,而ArrayList允许添加、插入或移除某一范围的元素;
(3)Array的下限可以自定义,而ArrayList的下限始终为零;
(4)Array可以具有多个维度,而ArrayList始终只是一维的;
(5)Array位于System命名空间中,ArrayList位于System.Collections命名空间中。
1.ArrayList的初始化
ArrayList有三个重载构造函数,其重载列表如表6-3所示。
表 6-3 ArrayList的构造函数重载列表
名称 | 说明 |
ArrayList() | 创建一个具有默认初始容量的ArrayList类的实例 |
ArrayList | 创建一个从指定集合复制元素并且具有与所复制的元素数相同的初始容量的ArrayList类的实例 |
(ICollection) | ArrayList类的实例 |
ArrayList(int) | 创建一个指定的初始容量的ArrayList类的实例 |
注意,ArrayList的容量是指能够容纳的元素个数,这里的容量并不是固定的,向ArrayList添加元素时,将根据需要自动增大容量。
创建动态数组对象的一般形式如下:
ArrayList 列表对象名 =new ArrayList([参数]);
例如:
ArrayList a = new ArrayList(); //创建一个拥有默认初始容量的ArrayList集合 ArrayList b = new ArrayList(5); //创建一个初始容量为5的ArrayList集合
ArrayList类提供了对象集合元素的常用操作,包括添加、删除、清空、插入、排序和反序以及压缩列表等操作方法,分别为Add、Remove、Clear、Insert、Sort、Reverse和TrimToSize。其中,压缩列表方法TrimToSize表示把集合大小重新设置为元素的实际个数。
2.向ArrayList中添加元素
ArrayList使用Add方法可以在集合的结尾处添加一个对象,Add方法的原型如下:
int Add(Object value) //添加一个对象到集合的末尾
该方法将返回添加了value处的索引值。另外,如果集合容量不足以保存新的对象,则会自动重新分配内部数组以增加存储容量,并在添加新元素之前将现有元素复制到新数组中。可以使用Count属性获取ArrayList中实际包含的元素个数。
例如:
ArrayList a = new ArrayList(); //创建一个拥有默认初始容量的ArrayLIst集合 Student stu = new Student("令狐冲",21); //创建一个Student对象 a.Add(stu); //在ArrayList集合Students中添加该对象
3.访问ArrayList中的元素
ArrayList集合与数组相同,只能通过索引来访问其中的元素,但不同的是,访问ArrayList中的元素时必须进行拆箱操作,即强制类型转换。其形式如下。
(类型)ArrayList(index)
例如,假设a是ArrayList集合,保存了若干个Student对象,则以下代码
Student x = (Student)a[0]; x.ShowMsg();
就是通过索引访问a集合中的第一个Student对象,最后调用其ShowMsg方法。
需要注意的是,由于ArrayList中允许添加Object类型的任意对象,在添加时,相当于一次装箱操作,所以在访问时,需要一次类型转换,把Object类型的对象转换成指定类型,这相当于一次拆箱。
4.删除ArrayList中的元素
ArrayList可以通过Remove、RemoveAt和Clear方法来删除ArrayList的元素,其形式为:
void Remove(Object obj) //删除指定对象名的对象 void RemoveAt(int index) //删除指定索引的对象 void Clear() //清楚集合内的所有元素
下面的示例展示了通过指定对象删除对象和通过索引删除对象的方法。
a.Remove(stu); //删除对象stu a.RemoveAt(1); //删除第二个对象
需要注意的时,ArrayList会动态调整索引,在删除一个元素后,该元素后面元素的索引值会自动减1.
例如,
Student x = new Student("令狐冲",1001); Student y = new Student("郭靖",1002); Student z = new Student("杨过",1003); a.Add(x); a.Add(y); a.Add(z); a.RemoveAt(1); //删除郭靖同学 a.RemoveAt(1); //删除杨过同学
上面的代码依次在ArrayList集合中添加了”令狐冲“、”郭靖“、"杨过"三位同学,执行”a.RemoveAt(1);“后删除了索引为1的同学,即郭靖同学后,杨过的索引调整为1,所以再次执行”a.RemoveAt(1);“后,将删除杨过同学,而如果再继续执行”a.RemoveAt(1);“,将出现”索引超出范围“的异常,因为此时集合中只有令狐冲同学,索引号为0.
5.向ArrayList中插入元素
可以使用Insert方法将元素插入到ArrayList的指定索引出。其形式为:
void Insert(int index,Object value) //元素插入到集合中的指定索引处
在插入元素时,ArrayList会自动调整索引,该元素后面元素的索引值会自动增加。
例如,
a.Insert(1,stu);
表示将stu插入到a集合中。
6.遍历ArrayList中的元素
ArrayList可以使用和数组类似的方式对集合中的元素进行遍历,例如:
for(int i = 0; i< a.Count ; i++) { Student x = (Student)a[i]; lblShow.Text +="\n" + x.ShowMsg(); }
也可以用foreach方式进行遍历,例如:
foreach(object x in a) { Student s =(Student)x; lblShow.Text += "\n" + s.ShowMsg(); }
下面的示例完整地展示了ArrayList的使用方法。
【实例 6-1】利用ArrayList进行集合的增、删、插入和遍历。

using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace 第六章 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } ArrayList a = new ArrayList(); //创建一个ArrayList集合 private void display() //显示所有学生的信息 { foreach(object x in a) //对集体a进行迭代处理 { Student s = (Student)x; richTextBox1.Text += "\n" + s.ShowMsg(); } } private void btnAdd_Click(object sender, EventArgs e) //在末尾添加元素 { int stuNo = Convert.ToInt32(textBox1.Text); Student x = new Student(textBox2.Text, stuNo); a.Add(x); //添加 richTextBox1.Text = ""; display(); } private void btnForeach_Click(object sender, EventArgs e) { richTextBox1.Text = ""; display(); } private void btnInsert_Click(object sender, EventArgs e) { int stuNo = Convert.ToInt32(textBox1.Text); int index = Convert.ToInt32(textBox3.Text); Student x = new Student(textBox2.Text, stuNo); a.Insert(index, x); //插入 richTextBox1.Text = ""; display(); } private void btnDelete_Click(object sender, EventArgs e) { int index = Convert.ToInt32(textBox3.Text); a.RemoveAt(index); //删除 richTextBox1.Text = ""; display(); } } public class Student { string name; int stuNo; public Student(string name,int no) //构造函数 { this.name = name; this.stuNo = no; } public string ShowMsg() //方法 { return string.Format("学号:{0},姓名:{1}", stuNo, name); } } }
【分析】当输入学号和姓名,单机”添加到末尾“按钮后,程序将根据输入的学生信息创建一个Student对象并添加到集合中的学生信息,也可以在索引框中输入索引值,单击”插入到“按钮,可根据输入的学生信息创建一个Student对象并插入到集合指定索引值位置;单击”删除“按钮,可将指定索引处的对象从集合中删除,单机”遍历“按钮,可将集合中的学生信息依次输出,运行效果如图6-1所示。
图 6-1 ArrayList使用效果
6.1.3哈希表Hashtable
哈希表又称散列表,Hashtable类是System.Collections命名空间的类,表示键/值对的集合。在使用哈希表保存集合元素(一种键/值对)时,首先要根据键自动计算哈希代码,以确定该元素的保存位置,再把元素的值放入相应的存储位置中。查找时,再次通过键计算哈希代码,然后到相应的存储位置中搜索,这样将大大减少为查找一个元素进行比较的次数。
创建哈希表对象的一般形式如下:
Hashtables 哈希表名 = new Hashtable([哈希表长度][,增长因子]);
其中,如果不指定哈希表长度,则默认容量为0,当向Hashtable中添加元素时,哈希表长度通过重新分配按需自动增加。增长因子表示每调整一次增加容量多少倍,默认的增长因子为1.0。
例如,
Hashtable a = new Hashtable();
表示创建了一个拥有默认初始容量和增长因子的Hashtable集合。
Hashtable类提供了哈希表常用操作方法,包括在哈希表中添加数据、移除数据、清空哈希表和检查是否包含某个数据等,方法名分别为Add、Remove、Clear和Contains。其中,Add方法需要两个参数,一个是键,一个是值。
下面的代码说明了如何向哈希表添加元素:
Student x = new Student("令狐冲",1001); //创建一个Student对象 a.Add(1001,stu); //将一个键为1001的Student对象添加到a集合
Remove方法只需要一个键名参数。
下面的代码表示将a集合中键值为1003的元素删除。
a.Remove(1003);
而获取哈希表的元素时,需要根据键去索引,并且和ArrayList一样,需要类型转换。下面的代码说明了如何根据键获取对应的值,即Student对象。
Student x =(Student)a[100]; //通过key获取元素 lblShow.Text = x.ShowMsg();
【实例 6-2】 利用Hashtable实现实例6-1类似的功能。

using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace 第六章 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } //ArrayList a = new ArrayList(); //创建一个ArrayList集合 Hashtable a = new Hashtable(); //创建一个Hashtable集合 private void display() //显示所有学生的信息 { foreach(object x in a.Values) //对集体a进行迭代处理 { Student s = (Student)x; richTextBox1.Text += "\n" + s.ShowMsg(); } } private void btnAdd_Click(object sender, EventArgs e) //在末尾添加元素 { int stuNo = Convert.ToInt32(textBox1.Text); Student x = new Student(textBox2.Text, stuNo); a.Add(stuNo,x); //添加集合元素,键号为学号,值为Student的引用 richTextBox1.Text = ""; display(); } private void btnForeach_Click(object sender, EventArgs e) { richTextBox1.Text = ""; display(); } private void btnDelete_Click(object sender, EventArgs e) { int key = Convert.ToInt32(textBox3.Text); a.Remove(key); //删除 richTextBox1.Text = ""; display(); } } public class Student { string name; int stuNo; public Student(string name,int no) //构造函数 { this.name = name; this.stuNo = no; } public string ShowMsg() //方法 { return string.Format("学号:{0},姓名:{1}", stuNo, name); } } }
【分析】当输入学号和姓名,单击”添加“按钮后,程序将根据输入的学生信息创建一个Student对象并添加到集合a中,并依次显示集合中的学生信息。连续输入三位学生信息后的运行效果如图 6-2 所示。也可以在”键“文本框中输入键值,单击”删除“按钮,可将指定键值的对象从集合中删除。
图 6-2 Hashtable运行效果
Hashtable可看作由Keys(键集)和Values(值集)这两个子集合组成的集合。在遍历哈希表时,既可以遍历其键集,也可以遍历其值集。本例就是遍历其值集。若要遍历其键集,则可使用以下类似代码。
foreach (object s_no in a.Keys) { int i =(int)s_no; Student x = (Student)a[i]; lblShow.Text+="\n"+x.ShowMsg(); }
6.1.4 栈和队列
1、栈Stack
Stack类实现了先进后出的数据结构,这种数据结构在插入或删除对象时,只能在栈顶插入或删除。
创建栈对象的一般形式如下:
Stack 栈名 = new Stack();
Stack类提供了栈常用操作方法,包括在栈顶添加数据、移除栈顶数据、返回栈顶数据、清空栈和检查是否包含某个数据等,方法名分别为Push、Pop、Peek、Clear、和Contains。其中,Push和Pop每操作一次只能添加或删除一个数据。
例如:
Stack s = new Stack(); s.Push("令狐冲"); s.Push("郭靖"); Console.WriteLine(s.Pop()); Console.WriteLine(s.Pop());
表示先创建一个栈对象s。然后将字符串“令狐冲”、“郭靖”添加到栈中,然后再将它们从栈中返回并删除。因此,最后程序的输出结果是:“郭靖”、“令狐冲”。
2.队列Queue
Queue类是一种先进先出的数据结构,这种数据结构把对象放进一个等待队列中,当插入或删除对象时,对象从队列的一端插入,从另一端移除。
队列可以用于顺序处理对象,因此队列可以按照对象插入的顺序来存储。
创建队列对象的一般形式如下:
Queue 队列名 = new Queue([队列长度][,增长因子]);
其中,队列长度默认为32,即允许队列最多存储32个对象。由于调整队列的大小需要付出一定的性能代价,因此建议在构造队列时指定队列的长度。增长因子默认为2.0,即每当队列容量不足时,队列长度调整为原来的2倍,可重新设置增长因子的大小。
例如,“Queue q = new Queue(50,3.0);”表示创建队列q,初始长度为50,可容纳50个对象,当容量不足时把队列长度调整为原来的3倍。
Queue类提供了队列常用操作方法,包括向队尾添加数据、移除队头数据、返回队头数据、清空队列和检查是否包含某个数据等,方法名分别为Enqueue、Dequeue、Peek、Clear和Contains。其中,Enqueue和Dequeue每操作一次只能添加或删除一个数据。
例如,
Queue q = new Queue(20,3.0f); q.Enqueue("令狐冲"); //进队 q.Enqueue("郭靖"); //进队 Console.WriteLine(q.Dequeue()); //出队 Console.WriteLine(q.Dequeue()); //出队
表示先在队列中添加两个字符串,然后重复调用Dequeue方法,按先进先出顺序返回并输出这两个字符串,程序的输出结果是:"令狐冲"、“郭靖”。
6.2 索引器
有时候,一个项目可能包含集合性的概念,当我们把这种集合概念抽象为类时,一定希望它能像数组一样通过索引进行访问,这样将方便程序设计。例如,一个相册对象a(即Album类的实例)包含多张照片(即Photo类的实例),所有照片存放在一个photos数组之中,此时访问第i张照片的一般形式是a.photos[i],但若能用一个索引i直接访问相册(即a[i])来获得第i张照片,那么这将使程序看起来更为直观,更容易编写。可以借助C#提供的索引器来实现。本节将详细介绍索引器的使用。
6.2.1索引器的定义
C#中的类成员可以是任意类型,包括数组和集合。当一个类包含数组或集合成员时,索引器将大大简化对数组或集合成员的存取操作。
定义索引器的方式与定义属性有些类似,其一般形式如下:
[修饰符] 数据类型 this[索引类型 index] { get { //获得属性的代码 } set { //设置属性的代码 } }
其中,修饰符包括public、protected、private等;数据类型是将要存取的数组或集合元素的类型;索引类型表示通过哪一种类型的索引来存取数组或集合元素,可以是整型,也可以是字符串等;this表示文本对象,可以简单地把它理解成索引器的名字,因此不能为索引器指定名称。与属性相同,索引器中包括get和set访问器,用来控制索引器的可读写操作,允许省略get和set访问器,以定义只读或只写的索引器。
例如,照片类和相册类的定义如下。
class Photo //定义一个照片类 { string _title; public Photo(string title) { this._title = title; } public string Title //只读属性,返回照片标题 { get { return _title; } } } class Album //定义一个相册类 { private Photo[] photos; //该数组用于存放照片 public Album(int capacity) //构造函数,初始化指定大小的Photo数组 { photos = new Photo[capacity]; } }
为了简化对相册类的photos数组的访问操作,在相册类中添加一个索引器来直接读写该数组,代码如下:
public Photo this[int index] //带有int参数的Photo读/写索引器 { get { if (index < 0 || index > photos.Length) //验证索引范围 { return null; //使用null指示失败 } return photos[index]; //对有效索引,返回请求的照片 } set { if (index < 0 || index >= photos.Length) { return; } photos[index] = value; } }
6.2.2 索引器的使用
一旦在一个类中定义了索引器,则通过该类的实例和索引就可以直接引用其中的数组元素或集合元素,一般形式如下。
对象名[索引]
其中,索引的数据类型必须与索引器的索引类型相同。
例如:
Album a = new Album(3); //创建一个容量为3的相册 a[0] = new Photo("张三丰的照片"); //通过索引把照片添加到相册中 Photo x = a[0]; //通过索引引用相册中的照片
但请注意,索引器只是简化了编程方式,系统最终执行的操作并没有真正改变。例如,当相册类的photos数组的可访问性修改为public时,上面的例子与以下代码是等效的。
Album a = new Album(3); //创建一个容量为3的相册 a.photos[0]= new Photo("张三丰的照片"); Photo x =a.photos[0];
6.2.3 索引器的重载
在C#中,索引器允许重载。通过重载索引器,可实现功能更强大的数据检索功能。例如,下面的代码将根据标题文字检索相册中的照片。
public Photo this[string title] //带有string参数的Photo只读索引器 { get { foreach(Photo p in photos) //遍历数组中的所有照片 { if(p.Title.IndexOf(title)!=-1) //返回符合条件的第一张照片 return p; } return null; //使用null指示失败 } }
以下代码直接根据照片标题文字检索相册中的照片。
Album a = new Album(3); //创建一个容量为3的相册 a[0] = new Photo("张三丰的小学照片"); //通过索引把照片添加到相册中 a[1] = new Photo("张三丰的中学照片"); //通过索引把照片添加到相册中 a[2] = new Photo("张三丰的工作照片"); //通过索引把照片添加到相册中 Photo x = a["小学照"]; //检索并引用相册中的照片
【实例 6-3】利用前面定义的索引器进行照片的添加和查询

using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace 第六章 { public partial class Test_6_3 : Form { public Test_6_3() { InitializeComponent(); } Album a = new Album(3); //创建一个容量为3的相册 private void btnAdd_Click(object sender, EventArgs e) { int i = Convert.ToInt32(index.Text) - 1; //索引从0开始 Photo p = new Photo(title.Text); //创建1张照片 a[i] = p; //向相册加载照片 richShow.Text = string.Format("照片添加成功!"); } private void btnShow_Click(object sender, EventArgs e) { int i = Convert.ToInt32(index.Text) - 1; Photo p = a[i]; //按索引检索 if (p != null) richShow.Text = string.Format("第{0}张照片的标题是:{1}", i + 1, p.Title); else richShow.Text = string.Format("没有第{0}张照片!", i + 1); } private void btnQuery_Click(object sender, EventArgs e) { Photo p = a[title.Text]; //按名称检索 if (p != null) richShow.Text = string.Format("找到标题为:{0}的照片!", p.Title); else richShow.Text = string.Format("没有找到标题为:{0}的照片", title.Text); } } class Photo //定义一个照片类 { string _title; public Photo(string title) { this._title = title; } public string Title //只读属性,返回照片标题 { get { return _title; } } } class Album //定义一个相册类 { private Photo[] photos; //该数组用于存放照片 public Album(int capacity) //构造函数,初始化指定大小的Photo数组 { photos = new Photo[capacity]; } public Photo this[int index] //带有int参数的Photo读/写索引器 { get { if (index < 0 || index > photos.Length) //验证索引范围 { return null; //使用null指示失败 } return photos[index]; //对有效索引,返回请求的照片 } set { if (index < 0 || index >= photos.Length) { return; } photos[index] = value; } } public Photo this[string title] //带有string参数的Photo只读索引器 { get { foreach(Photo p in photos) //遍历数组中的所有照片 { if(p.Title.IndexOf(title)!=-1) //返回符合条件的第一张照片 return p; } return null; //使用null指示失败 } } } }
【分析】本例首先创建一个容量为3的Album的相册对象a,当在“照片标题”文本框中输入“张三丰的小学照片”,并在后面的文本框中输入“1”后,单击“添加到”按钮,程序将创建一个photo对象,并通过索引器添加到a的photos数组索引为0的位置。再依次输入“张三丰的中学照片”、”张三丰的工作照片“,并同时在后面的文本框中输入”2“、”3“,单击”添加到“按钮,完成a的photos数组的初始化。这时在”照片标题文本框中输入工作照“,单击”按标题查找“按钮后,系统执行模糊查询,运行效果如图6-3所示。在右边的文本框输入需要显示的照片编号,单击”显示第“按钮也可以使用索引器访问指定索引的照片。
图 6-3 索引器运行效果
6.2.4 接口中的索引器
在接口中也可以声明索引器,接口索引器与类索引器有两个区别:一是接口索引器不使用修饰符;二是接口索引器只包含访问器get或set,没有实现语句,其用途是指示索引器是可读写、只读还是只写的,如果是可读写的,访问器get和set均不能省略;如果是只读的,省略set访问器;如果是只写的,省略get访问器。
例如:
interface IAlbum { Photo this[int index]{get;set;} //声明索引器 Photo this[string title]{set;} //声明可读写索引器 int count{get;} //声明属性,返回相册中照片个数 bool Remove(int index); //声明方法,删除指定照片 string getTitle(int index); //声明方法,返回指定照片的标题 }
表示所有声明的接口IAlbum包含5个成员:两个索引器、一个属性和两个方法。
6.2.5 索引器与属性的比较
索引器与属性都是类的成员,语法上非常类似。索引器一般用在自定义的集合类中,通过使用索引器来操作集合对象就如同使用数组一样简单;而属性可用于任何自定义类,它增强了类的字段成员的灵活性。表6-7列出了索引器与属性的主要区别。
表 6-7 索引器与属性的区别
属性 | 索引器 |
允许调用方法,如同公共数据成员 | 允许调用对象上的方法,如同对象是一个数组 |
可通过简单的名称进行访问 | 可通过索引器进行访问 |
可以为静态成员或实例成员 | 必须为实例成员 |
其get访问器没有参数 | 其get访问器具有与索引器相同的形参表 |
其set访问器包含隐式value参数 | 除了value参数外,其set访问器还具有与索引器相同的形参表 |
6.3 泛型
泛型是从C# 2.0时引入的一个新功能,泛型是通过”参数化类型“来实现在同一段代码中操作多种数据类型。泛型类型是一种编程范式,它利用”参数化类型“将类抽象化,从而实现更为灵活的复用。泛型赋予了代码更强的安全性、更好的复用、更高的效率和更清晰的约束。本节将介绍泛型的基本使用 方法。
6.3.1 泛型概述
通常在讨论数组或集合时都需要预设一个前提,即到底要解决的是整数、小数,还是字符串的运算问题。因此,在使用数组时需要首先确定数组的类型,然后再把相同类型的数据存入数组中。例如,把100个整数存入数组中,得到一个整型数组,而把100个自定义的Student对象存入数组中,得到一个Student型数组。
利用数组来管理数据,虽然直观、容易理解,但存在很大的局限性,仍然需要重复编写几乎完全相同的代码来完成排序和查找操作。为此,C#提供了一种更加抽象的数据类型——泛型,以克服数组的不足。当利用泛型来声明这样一个更为抽象的数据类型之后,再也不需要针对诸如整数、小数、字符、字符串等数据重复编写几乎完全相同的代码。
泛型的另一个优点是”类型安全“,上面提到的集合类是没有类型化的,以ArrayList为例,继承自System.Object的任何对象都可以存储在ArrayList中。
例如,以下代码都是正确的。
ArrayList list = new ArrayList(); list.Add(44); list.Add("mystring"); list.Add(new Student("令狐冲",1001));
如果使用下面的foreach语句遍历上面的list集合:
foreach(object o in list) { Console.WriteLine((int)o ); }
则C#编译器会编译通过这段代码。但是,由于有些集合元素是不能转换为int的,因此程序在运行时会出现异常。
如果采用泛型,则可以较早地检查放入集合中地元素是否是预定的类型,以保证类型安全。
.NET Framework在System.Collections.Generic和System.Collecions.ObjectMode命名空间中提供了大量的泛型集合类,如List、Queue、Stack、Dictionary等,这些集合lei基本上都提供了增加、删除、清除、排序和返回集合元素值的操作方法,这些操作方法对任意类型的数据都有效。
6.3.2 泛型集合
泛型最常见的用途是创建集合类,泛型集合可以约束集合内的元素类型。典型的泛型集合包括List<T>、Dictionary<K,V>等。
1.List<T>
列表List<T>是动态数组ArrayList的泛型等效类,是强化类型化的列表。因为.NET Framework在定义List<T>时没有指定集合元素的类型,只是用参数”T“来代表未来的集合元素的类型,因此在使用List<T>时,必须明确指定数据类型。
创建一个列表对象的格式如下:
List<元素类型>对象名 = new List<元素类型>();
在使用List<T>时,要注意引入命名空间:System.Collecions.Greneric。
例如,
List<Student>list =new List<Student>();
表示创建了一个泛型集合并指定该集合只能存放Student类型的元素。
List<T>与ArrayList的使用方法相似。在创建了列表对象之后,可调用其内置的方法添加和删除数据元素。
例如,
Student x= new Student("令狐冲",101); //创建一个Student对象 list.Add(x); //添加到list泛型集合
表示将Student型的对象x添加到已创建的泛型集合对象list之中。
注意,在list中只能添加Student型的对象,否则将出现编译错误。
【思考】 请读者思考以下语句是否正确?
list.Add(103);
在访问泛型集合元素时,因为泛型集合是强类型的集合,所以无需类型转换。
例如,以下代码说明了通过索引访问元素和通过foreach遍历集合时都不需要类型转换。
Student x = list[0]; //使用索引访问,无需类型转换 lblShow.Text = x.ShowMsg(); foreach(Student s in list) { lblShow.Text = s.ShowMsg(); //遍历时不许哟啊类型转换 }
与ArrayList一样,List<T>提供RemoveAt成员方法,调用该方法可删除指定索引的元素。例如:
lis.RemoveAt(0); //利用索引删除
可见,List<T>与ArrayList的相同之处是,它们都用Add和RemoveAt等方法来添加和删除数据元素,都通过索引访问数据元素。不同之处有两点:一是在ArrayList中可以添加任何类型的数据元素,而在List<T>中只能添加指定类型的数据元素;二是在访问集合元素时ArrayList集合需要拆箱访问,而List<T>无需拆箱即可直接访问。
2.Dictionary<K,V>
字典Dictionary是键和值的集合,它实质上仍然是一个哈希表,只是在使用时要指定键和值的类型。其中,K和V就表示数据元素的键(key)与值(value)的数据类型。与List<T>相同,Dictionary<K,V>集合在编译时要检查是否指定了明确的数据类型,在访问集合元素时也无需拆箱操作。
创建一个字典对象的格式如下:
Dictionary<键类型,值类型>对象名 = new Dictionary<键类型,值类型>();
例如,
Dictionary<int,Student>dic = new Dictionary<int,Student>();
表示创建一个字典集合,并指定该集合中Key为int型、Value为Student型。
Dictionary<K,V>与Hashtable的使用方法相似,在添加数据元素时必须指定元素的键和值。例如:
Student x = dic[101]; //通过key获取元素,无需类型转换 lblShow.Text = x.ShowMsg(); foreach(Student s in dic.Values) //遍历集合的Values子集 { lblShow.Text = s.ShowMsg(); //遍历时不需要类型转换 }
与Hashtable一样,Dictionary<K,V>也使用Remove方法来删除指定key的元素。例如:
dic.Remove(101); //通过Key删除元素
6.3.3 自定义泛型
C#允许自定义泛型,包括自定义泛型类、泛型方法和泛型接口等。
1.泛型类
当一个类的操作不针对特定或具体的数据类型时,可把这个类声明为泛型类。泛型类通常用来描述抽象的具有集合性质的数据结构,如链表、哈希表、堆栈、队列和树等。对初学者来说,因为泛型难以理解,因此建议从一个现有的具体类开始,逐一将每个类型更改为类型参数,一直达到通用化和可用性的最佳平衡,从而理解泛型类的定义。
1)泛型类的定义
定义泛型类的一般形式如下:
[访问修饰符]class 泛型类名<类型参数列表>
{
//类的成员
}
其中,”访问修饰符“包括public和internal等。”类型参数列表“表示不确定的数据类型及其个数,当不确定的数据类型不止一个时,类型参数之间用逗号分隔。类型参数的名称必须遵循C#的命名规则,常用K、V和T等字母来表示
例如:
public class Person<T> { ....... }
其中,T表示一种不确定的数据类型。
泛型类可以包含任意多个不确定的数据类型,它们之间用逗号分隔开。
例如:
public class Person<T1,T2,T3> { ... }
在泛型类中一旦声明了类型参数,就可以像使用标准类型一样使用,可以用作成员字段、属性、方法返回值类型,也可以用作方法的参数类型等。
例如,以下代码先声明类型参数T1和T2,然后用它们声明字段变量和形参变量。
public class Person<T1,T2> { T1 t1; T2 t2; public Person(T1 x,T2 y) { t1=x; t2=y; } public string ShowMsg() { return string.Format("{0}:{1}",t1,t2); } }
2)泛型类的使用
在定义泛型类时指定的类型参数只是一种临时的标识符,在使用泛型类时必须用明确的类型代替类型参数。
例如,以下代码:
Person<int,string>student = new Person<int,string>(1001,"令狐冲"); lblShow.Text+="\n"+student.ShowMsg();
指定T1为int,指定T2为string,分别用来表示学生的学号和姓名。
而以下代码:
Person<string,string>teacher = new Person<string,string>("教授","洪七公"); lblShow.Text+="\n"+teacher.ShowMsg();
指定T1和T2均为string,分别用来表示教师的职称和姓名。
2.泛型方法
泛型方法是在泛型类或泛型接口中使用类型参数声明的方法。其一般形式如下:
[访问修饰符]返回值类型 方法名<类型参数列表>(形参列表)
{
//语句
}
其中,类型参数列表与其所属的泛型类的类型参数列表相同。
例如,
void Swap<T>(ref T x,ref T y) { T t; t=x; x=y; y=t; }
该方法就是一个泛型方法,其返回值类型为void,方法名为Swap,类型参数列表只有一个T。以下代码展示了如何调用该方法:
int a = 5,b=8; Swap<int>(ref a,ref b);
3.泛型接口
泛型接口通常用来为泛型集合类定义接口。对于泛型类来说,从泛型接口派生可以避免值类型的装箱和拆箱操作。.NET Framework类库定义了若干个新的泛型接口,在System.Collections.Generic命名空间中的泛型集合类(如List和Dictionary)都是从这些泛型接口派生的。如表 6-8列举了.NET Framework中常用的泛型接口。
表 6-8 常用的泛型接口
接口 | 说明 |
ICollection | 定义操作泛型集合的方法 |
IComparer | 定义比较两个对象的方法 |
IDictionary | 表示键/值对的泛型集合 |
IEnumberable | 公开枚举数,该枚举数支持在指定类型的集合上进行简单的迭代 |
IEqualityComparer | 定义方法,以支持对象的相等比较 |
IList | 表示可按照索引单独访问的一组对象 |
C#允许自定义泛型接口,一般形式如下:
[访问修饰符] interface 接口名<类型参数列表>
{
//接口成员
}
其中,访问修饰符可省略,”类型参数列表“表示尚未确定的数据类型,类似于方法中的形参列表,多个类型参数之间用逗号分隔,泛型接口也可以使用类型约束。
例如:
interface IDate<T> { }
表示声明了一个名为IDate的泛型接口,它包含一个类型参数T。
【实例 6-4】设计一个泛型类,实现任意类型的数据排序。

using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace 第六章 { public partial class Test6_4 : Form { public Test6_4() { InitializeComponent(); } private void Test6_4_Load(object sender, EventArgs e) { Data<int> a = new Data<int> (3, 5, 3, 6, 7, 12 ); //创建泛型类的实例,指定类型参数T为int a.sort(); lblShow.Text = a.display(); //创建泛型类的实例,指定类型参数T为int Data<float> b = new Data<float>(3.5f, 5.5f, 3.4f, 9.9f, 6.7f, 1.2f); b.sort(); lblShow.Text += b.display(); } } class Data<T> //定义泛型类,设类型参数为T { private T[] datas; //使用类型参数T声明数组datas public Data(params T[] x) //构造函数,设置其形参为params数组 { datas = x; } public void sort() //排序方法 { for(int i=0;i<datas.Length;i++) //从大到小排序 { int k = i; for(int j=i;j<datas.Length;j++) { if (Convert.ToDouble(datas[k]) < Convert.ToDouble(datas[j])) k = j; } if (k != i) { T t = datas[i]; datas[i] = datas[k]; datas[k] = t; } } } public string display() //输出数组元素的方法 { string info = "\n"; for(int i = 0; i < datas.Length; i++) { info+=datas[i].ToString() + " "; } return info; } } }
【分析】本程序首先定义了一个泛型类Data<T>,T是临时的类型标识符,在引用泛型类时必须明确指定为int、float或double等。在该泛型类中声明了一个数组datas,其数据类型为类型参数T。该泛型类的构造函数负责初始化数组datas,sort方法实现datas数组元素从大到小排序,display方法用来输出数组元素。在本例中的窗体类Test6_4的Load事件方法中,先后使用泛型类Data<T>定义了两个变量a和b,并分别指定类型参数T为int和float。在初始化a和b时分别指定要排序的数据列表,当执行a.sort和b.sort之后,其内部数组datas中的数据元素都将重新降序排列。本程序的运行效果如下。
6.3.4 泛型的高级应用
1.约束泛型类的类型参数
在定义泛型类时,有时需要指定只有某种类型的对象或从这个类型派生的对象可被用作类型参数。这时,可以使用where关键字来约束类型参数的类型。
例如:
public class Animal //动物类 { } public class Plant //植物类 { } public class Dog:Animal //狗类 { } public class Pet<T> where T:Animal //宠物类 { }
在本例中,Pet类为一个泛型类,尖括号中的T即为类型参数。本例使用where关键字对T进行约束,限制T必须是一个与Animal有关的类,即只有Animal和派生类Dog可以作为类型参数,而Plant不能作类型参数。
在C#中,一共有5类约束,分别如下。
(1)where T:strict:类型参数必须是值类型。
(2)where T:class:类型参数必须是引用类型,包括任何类、接口、委托等。
(3)where T:new():类型参数必须具有无参数的构造函数,当与其他约束一起使用时,该约束必须最后指定。
(4)where T:类名:类型参数必须是指定类及其派生类。
(5)where T:接口名:类型参数必须是指定的接口或实现指定的接口。可以指定多个接口约束。
2.泛型类的继承性
泛型类也具有继承性,C#允许从一个已有的泛型类派生新的泛型类。
例如,
public class MyPet<T>:Pet<T> { }
其中,MyPet<T>就是一个派生类。
注意,如果基泛型类在定义时指定了约束,则从它派生的类型也将受到约束,而不能”解除约束“,当然派生的泛型类还可以指定更严格的约束。
例如:
public class Mypet<T>:Pet<T>where T:Dog { }
因为在前文中Pet<T>类的类型参数T被约束为Animal,Dog又是Animal的一个派生类,因此将派生类MyPet<T>的类型参数T指定为Dog类是合法的。
【实例 6-5】泛型的定义和使用演示。

using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace 第六章 { public partial class Test6_5 : Form { public Test6_5() { InitializeComponent(); } Pet<Animal> myPet = new Pet<Animal>(); private void btnAddDog_Click(object sender, EventArgs e) { myPet.Animals.Add(new Dog(txtName.Text)); richTxtShow.Text += string.Format("\n添加Dog:{0}成功", txtName.Text); } private void btnAddLittleDog_Click(object sender, EventArgs e) { myPet.Animals.Add(new SmallDog(txtName.Text)); richTxtShow.Text += string.Format("\n添加SmallDog:{0}成功", txtName.Text); } private void btnAddCat_Click(object sender, EventArgs e) { myPet.Animals.Add(new Cat(txtName.Text)); richTxtShow.Text += string.Format("\n添加Cat:{0}成功", txtName.Text); } private void btnFeed_Click(object sender, EventArgs e) { richTxtShow.Text = myPet.FeedTheAnimals(); } } public abstract class Animal //抽象类 { protected string name; public Animal(string name) { this.name = name; } public abstract string Eat();//抽象方法 } public abstract class Plant //抽象类 { public abstract string Eat(); } public abstract class Rose : Plant //抽象类 { public override string Eat() { return "Rose:我要喝水!"; } } public class Dog : Animal //派生类 { public Dog(string name) : base(name) { } public override string Eat() { return string.Format("{0}:我是Dog,我要吃骨头!",name); } } public class Cat : Animal //派生类 { public Cat(string name) : base(name) { } public override string Eat() { return string.Format("{0}:我是Cat:我要吃鱼!",name); } } public class SmallDog: Dog //派生类 { public SmallDog(string name) : base(name) { } public override string Eat() { return string.Format("{0}:我是SmallDog:我要吃狗粮!", name); } } public class Pet<T>where T : Animal //泛型类 { private List<T> animals = new List<T>(); //泛型字段成员 public List<T> Animals //泛型的属性成员 { get { return animals; } } public string FeedTheAnimals() { string msg = string.Empty; foreach(T x in animals) { msg += "\n" + x.Eat(); } return msg; } } }
【分析】在该程序中,首先定义了一个抽象基类Animal,然后定义了两个派生类:Dog和Cat,并从Dog派生出SmallDog类。派生类重写了基类的Eat方法。之后定义了一个泛型类Pet<T>,该泛型类中包含一个List<T>的泛型字段(该字段是一个集合对象,用来存放Animal对象),一个只读的泛型属性,以及一个方法(用来迭代调用List<T>集合中的每个元素Eat方法)。在声明该泛型类时,使用where关键字对类型参数T进行约束,表示该集合只能存取Animal对象或派生类对象。在窗体类Test6_5中,先使用泛型类创建一个myPet对象,当在文本框中输入宠物的名字后,可以单击”添加狗“、”添加小狗“或”添加猫“按钮,把对应的实例添加到myPet的泛型集合中。最后,单击”喂食“按钮,程序将依次显示每个宠物的名字和它要吃的东西。该程序的运行效果如图6-4所示。
图 6-4 泛型类运行效果
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?