1、1第 2 章 集合与泛型2.1 集合概述所谓集合是特殊元素们的一种聚合。集合的元素被称为是成员。集合有两个最重要的属性,一个是集合成员都是无序的,另一个则是集合的成员不会出现超过一次。在计算机科学领域内集合扮演着非常重要的角色,但是不把集合包含作为 C#语言的一种数据结构。2.1.1 集合的定义人们把集合定义成相关成员的无序聚集,而且集合中的成员不会出现超过一次。集合书写成用一对闭合大括号包裹成员列表的形式,例如0,1,2,3,4,5,6,7,8,9。只要全部成员只书写一次,就可以按照任意顺序书写集合,所以此前的集合实例还可以写成9,8,7,6,5,4,3,2,1,0或其他任意成员组合的形式。
2、为了使用集合需要知道一些有关集合的定义。1. 不包含任何成员的集合称为空集合。全域是所有可能成员的集合。2. 如果两个集合包含完全一样的成员,那么就认为这两个集合相等。3. 如果第一个集合的全部成员都包含在第二个集合内,就认为第一个集合是第二个集合的子集。2.1.2 集合的操作下面描述了在集合上执行的基本操作。1. 联合:由第一个集合中的所有成员和第二个集合中不包含在第一个集合的成员组成的新集合。2. 交叉:由既属于第一个集合又属于第二个集合的成员组成的新集合。3. 差异:由属于第一个集合但不属于第二个集合的成员组成的新集合。2.2 集合类ArrayList 是与数组相当的集合类。还有其他类型
3、的集合:队列、栈、有序表和哈希表,如下所示:2 ArrayList 动态数组 Queue 队列 Stack 栈 BitArray 位数组 Hashtable 散列表 SortedList 有序表集合类位于 System.Collections 命名空间,集合类可以组合为集合,存储 Object 类型的元素。2.3 集合的一种实现HashTable 类是.NET 框架类库中较为有效的数据结构之一,它的存取速度较其它类而言要快。下面就以散列表作为存储结构来实现一个集合类,具体代码如下:using System;using System.Collections;/集合public class Set
4、private Hashtable data;public Set()data = new Hashtable();/添加成员。如果成员不在集合内,则添加到集合中public void Add(Object item)if (!data.ContainsValue(item)data.Add(Hash(item), item);/根据成员各字符的 ASCII 码值计算散列值(在散列表中数据项以键值对形式存在)3private string Hash(Object item)char chars;string s = item.ToString();int hashValue = 0;chars
5、 = s.ToCharArray();for (int i = 0; i aSet.Size) return false;foreach (Object key in this.data.Keys)if (!(aSet.data.Contains(key) return false;return true;7/求差集。由属于第一个集合但不属于第二个集合的成员组成的新集合。public Set Difference(Set aSet)Set tempSet = new Set();foreach (Object hashObject in data.Keys)if (!(aSet.data.Co
6、ntains(hashObject)tempSet.Add(datahashObject);return tempSet;public override string ToString()string s = “;foreach (Object key in data.Keys) s += datakey + “ “;return s;class Teststatic void Main()Set setA = new Set();setA.Add(“中国“);setA.Add(“日本“);setA.Add(“美国“);setA.Add(“韩国“);Set setB = new Set();s
7、etB.Add(“日本“);setB.Add(“韩国“);setB.Add(“加拿大“);Set setC = new Set();8setC = setA.Union(setB);Console.WriteLine(“A: “ + setA.ToString();Console.WriteLine(“B: “ + setB.ToString();Console.WriteLine(“A union B: “ + setC.ToString();setC = setA.Intersection(setB);Console.WriteLine(“A intersect B: “ + setC.T
8、oString();setC = setA.Difference(setB);Console.WriteLine(“A diff B: “ + setC.ToString();setC = setB.Difference(setA);Console.WriteLine(“B diff A: “ + setC.ToString();if (setB.Subset(setA)Console.WriteLine(“B is a subset of A“);elseConsole.WriteLine(“B is not a subset of A“);Console.Read();运行结果如下:92.
9、1 泛型概述泛型(Generic Type)是.NET Framework 2.0 的一大特性。泛型的主要思想就是将算法与数据结构完全分离开来,使得一次定义的算法能够作用于多种数据结构,从而实现高度可重用的开发。通过泛型可以定义类型安全的数据结构,而没有必要使用实际的数据类型。这将显著提高性能并得到更高质量的代码,因为可以重用数据处理算法,而没有必要复制类型特定的代码。泛型是一个很强大的特性,对于集合类而言尤其如此。.NET 1.1 中的大多数集合类都基于 Object 类型。.NET 从 2.0 开始提供了实现为泛型的新集合类。泛型不仅限于类,还可用于委托、接口和方法的泛型。在.NET 2.
10、0 之前,不存在泛型,在开发通用容器时,需要通用容器能存储各种类型的实例。在.NET Framework 1.1 下,必须使用基于 object 类型的系统集合类(例如ArrayList,Stack ,Queen 等) ,但由于集合类中的项都是 Object 类型的,因此每次使用时都必须进行装箱拆箱操作,不仅大大降低了程序的性能,而且存在类型安全问题(Object 类在编译期间没有类型安全性) 。泛型(Generic Type)的出现较好的解决了上述问题,通过它,可以轻松创建指定类型的集合。泛型集合类不仅是类型安全的,而且使用时不需要进行烦琐的装箱拆箱操作。泛型是 C# 2.0 中的新增元素(
11、类似于 C+中模板) 。这种机制允许将类名作为参数传递给泛型类型,并生成相应的对象。主要利用 System.Collections.Generic 命名空间下面的 List 泛型类来创建集合。语法如下:List ListOfT = new List( );其中的“T“就是所要使用的类型,既可以是简单类型,如 string、int,也可以是用户自定义类型。下面看一个具体例子。10using System;using System.Collections.Generic;struct Studentpublic string sno; /学号public string sname; /姓名publ
12、ic int sage; /年龄public Student(string sno, string sname, int sage)this.sno = sno;this.sname = sname;this.sage = sage;class SamplesGenericstatic void Main()/创建 Student 对象Student stu1 = new Student(“001“, “张三“, 30);Student stu2 = new Student(“002“, “李四“, 20);Student stu3 = new Student(“003“, “王五“, 50)
13、;/创建类型为 Student 的对象集合List students = new List();/将 Student 对象放入集合students.Add(stu1);students.Add(stu2);students.Add(stu3);/输出第 1 学生的姓名Console.WriteLine(students0.sname);11运行结果如下图:下面介绍泛型的优点:1. 性能泛型的一个主要优点是性能。对值类型使用 System.Collections 命名空间的集合类,存储数据时需要把值类型转换为引用类型,获取数据时需要把引用类型转换为值类型,即需要进行装箱和拆箱操作。例如,下面例子
14、在存取数据时就进行了装箱拆箱操作。ArrayList list = new ArrayList();list.Add(44); / boxing - convert a value type to a reference typeint i1 = (int)list0; / unboxing - convert a reference type to a value typeforeach (int i2 in list)Console.WriteLine(i2); / unboxing装箱和拆箱操作很容易使用,但性能损失比较大,迭代许多项时尤其如此。System.Collections.Ge
15、neric 命名空间中的 List类不使用对象,而是在使用时定义类型。在下面的例子中,List类的泛型类型定义为 int,所以 int 类型在 JIT 编译器动态生成的类中使用,不再进行装箱和拆箱操作:List list = new List();list.Add(44); / no boxing - value types are stored in the Listint i1 = list0; / no unboxing, no cast neededforeach (int i2 in list)Console.WriteLine(i2);2. 类型安全泛型的另一个特性是类型安全。与
16、ArrayList 类一样,如果使用对象,可以在这个集合中添加任意类型。下面的例子在 ArrayList 类型的集合中添加一个整数、一个字符串和一个 MyClass 类型的对象:ArrayList list = new ArrayList();12list.Add(44);list.Add(“mystring“);list.Add(new MyClass();如果这个集合使用下面的 foreach 语句迭代,而该 foreach 语句使用整数元素来迭代,编译器就会编译这段代码。但并不是集合中的所有元素都可以转换为 int,所以会出现一个运行异常:foreach (int i in list)C
17、onsole.WriteLine(i);错误应尽早发现。在泛型类 List中,泛型类型 T 定义了允许使用的类型。有了List的定义,就只能把整数类型添加到集合中。编译器不会编译这段代码,因为Add()方法的参数无效:List list = new List();list.Add(44);list.Add(“mystring“); / compile time errorlist.Add(new MyClass(); / compile time error3. 代码重用泛型允许更好地重用代码。泛型类可以定义一次,用许多不同的类型实例化。例如,System.Collections.Generi
18、c 命名空间中的 List类用一个 int、一个字符串和一个 MyClass 类型实例化:List list = new List();list.Add(44);List stringList = new List();stringList.Add(“mystring“);List myclassList = new List();myClassList.Add(new MyClass();泛型类型可以在一种语言中定义,在另一种.NET 语言中使用。2.2 创建泛型类泛型最显著的一点就是它参数化了类型,把类型作为参数抽象出来,从而使我们在实际的运用当中能够更好的实现代码的重复利用,同时它提供了
19、更强的类型安全,更高的效率,不过在约束方面,它只支持显式的约束,这样在灵活性方面就显得不是那么好了。泛型之所以能够提供更高的效率是因为泛型在实例化的时候采用了“on-demand“的模式,即按需实例化,发生在 JIT(Just In Time)编译时。13下面来看如何定义一个泛型类,很简单,你只需要意识到一点类型在这里被参数化了:using System;using System.Collections;using System.Collections.Generic;/ / 定义一个泛型类,该类有一个类型参数 T/ / 类型参数 class Item/泛型类的类型参数可用于类成员public
20、 T Value;public Item Next;public Item(T value)this.Value = value;this.Next = null;class Collection : IEnumerablepublic Item First = null;public Item Last = null;public void Add(T value)Item newItem = new Item(value);if (this.First = null)this.First = newItem;this.Last = newItem;elsethis.Last.Next =
21、newItem;14this.Last = newItem;public IEnumerator GetEnumerator()Item current = this.First;while (current != null)yield return current.Value;current = current.Next;IEnumerator IEnumerable.GetEnumerator()return GetEnumerator();class SamplesGenericstatic void Main()/使用 string 来实例化 Collection类Collection
22、 names = new Collection(); /调用泛型类中的方法names.Add(“张三“);names.Add(“李四“);names.Add(“王五“);foreach (string name in names)Console.WriteLine(name);/同样,也可以使用 int 来实例化 Collection类Collection ages = new Collection();15/调用泛型类中的方法ages.Add(25);ages.Add(34);ages.Add(47);foreach (int age in ages)Console.WriteLine(ag
23、e);Console.Read();使用泛型类 Collection,相当于定义了一个类模板,在具体使用时,需要什么类型即可将它实例化为什么类型,从而实现了代码重用。由于可以用任何类型实例化它,且无需装箱操作,因此比基于 Object 的非泛型方式获取了更高的性能。如果将Collection实例化为 int 类型,则使用 Add ()方法传送非 int 值,就会出现一个编译错误,所以说它是类型安全的。使用泛型 IEnumerable, foreach 语句也是类型安全的,如果 foreach 语句中的变量不是 int,也会出现一个编译错误。2.3 泛型类的特性在创建泛型类时,需要一些其他 C#
24、关键字。例如,不能把 null 赋予泛型类型。此时,可以使用 default 关键字。如果泛型类型不需要 Object 类的功能,但需要调用泛型类上的某些特定方法,就可以定义约束。1. 默认值在创建泛型类时,如果需要给类型 T 指定 null,但是,不能把 null 赋予泛型类型。原因是泛型类型也可以实例化为值类型,而 null 只能用于引用类型。为了解决这个问题,可以使用 default 关键字。通过 default 关键字,将 null 赋予引用类型,将 0 赋予值类型。using System;using System.Collections;using System.Collectio
25、ns.Generic;class SamplesGenericpublic T Value;16public SamplesGeneric() this.Value = default(T);/如果 T 是值类型则 default(T)=0;如果 T 是引用类型则 default(T)=null ;class Teststatic void Main()SamplesGeneric obj1 = new SamplesGeneric();Console.WriteLine(obj1.Value);SamplesGeneric obj2 = new SamplesGeneric();Consol
26、e.WriteLine(obj2.Value);Console.Read();注意:default 关键字根据上下文可以有多种含义。 switch 语句使用 default 定义默认情况。在泛型中,根据泛型类型是引用类型还是值类型,default 关键字用于将泛型类型初始化为 null 或 0。2. 约束C#中的泛型只支持显式的约束,因为这样才能保证 C#所要求的类型安全,但显式的约束并非时必须的,如果不加约束,泛型类型参数将只能访问 System.Object 类型中的公有方法。 “显式约束”由 where 子句表达,可以指定“基类约束” , “接口约束” ,“构造器约束” , “值类型/引
27、用类型约束 ”共四种约束。1、基类约束:class A public void F1() class B public void F2() class C where S: A / S 继承自 A where T: B / T 继承自 B 17 / 可以在类型为 S 的变量上调用 F1,/ 可以在类型为 T 的变量上调用 F2 2、接口约束interface IPrintable void Print(); interface IComparable int CompareTo(T v);interface IKeyProvider T GetKey(); class Dictionary w
28、here K: IComparable where V: IPrintable, IKeyProvider / 可以在类型为 K 的变量上调用 CompareTo, / 可以在类型为 V 的变量上调用 Print 和 GetKey 3、构造器约束class A public A() class B public B(int i) class C where T : new() /可以在其中使用 T t=new T(); C c=new C(); /可以, A 有无参构造器C c=new C(); /错误,B 没有无参构造器4、值/引用类型约束public struct A public cla
29、ss B class C where T : struct / T 在这里面是一个值类型 C c=new C(); /可以, A 是一个值类型C c=new C(); /错误,B 是一个引用类型18提示:在 C#中,where 子句的一个重要限制是,不能定义必须由泛型类型执行的运算符。运算符不能在接口中定义。在 where 子句中,只能定义基类、接口和默认构造函数。3. 继承如何实现泛型类的继承呢?这里需要满足下面两点中的任何一点即可:1、泛型类继承中,父类的类型参数已被实例化,这种情况下子类不一定必须是泛型类;2、父类的类型参数没有被实例化,但来源于子类,也就是说父类和子类都是泛型类,并且二
30、者有相同的类型参数;/如果这样写的话,显然会报找不到类型 T 的错误class SubItem : Item /正确的写法应该是class SubItem : Item class SubItem : Item class SubItem : Item 4. 静态成员泛型类的静态成员只能在类的一个实例中共享。下面看一个例子。using System;using System.Collections.Generic;class SamplesGenericpublic static T Value;class Teststatic void Main()SamplesGeneric.Value
31、= 5;Console.WriteLine(SamplesGeneric.Value);SamplesGeneric.Value = “hello“;Console.WriteLine(SamplesGeneric.Value);Console.Read();192.4 泛型接口泛型接口的创建以及继承规则和泛型类是一样的,看下面的代码:using System;using System.Collections.Generic;interface IAdderT Add(T v1, T v2);class Adder1 : IAdderpublic int Add(int v1, int v2)
32、return v1 + v2;class Adder2 : IAdderpublic string Add(string v1, string v2)return v1 + v2;class SamplesGenericstatic void Main()Adder1 adder1 = new Adder1();int sum = adder1.Add(5, 7);Console.WriteLine(sum);20Adder2 adder2 = new Adder2();string str = adder2.Add(“hello“, “world“);Console.WriteLine(st
33、r);Console.Read();2.5 泛型方法我们再来看泛型方法,C#的泛型机制只支持在方法声明上包含类型参数,也即是泛型方法。特别注意的是,泛型不支持在除了方法以外的其他类/接口成员上使用类型参数,但这些成员可以被包含在泛型类型中,并且可以使用泛型类型的类型参数。还有一点需要说的就是,泛型方法可以在泛型类型中,也可以存在于非泛型类型中。下面通过例子看一下泛型方法的声明与调用。using System;using System.Collections.Generic;class Exchange/声明一个泛型方法public static void Swap(ref T x, ref T
34、 y)T temp;temp = x;x = y;y = temp;class SamplesGenericstatic void Main()int i = 4;int j = 5;21/在调用泛型方法时,对泛型方法的类型参数实例化Exchange.Swap(ref i, ref j);Console.WriteLine(“i = 0, j = 1“, i, j);/泛型方法可以像非泛型方法那样调用Exchange.Swap(ref i, ref j);Console.WriteLine(“i = 0, j = 1“, i, j);Console.Read();2.6 泛型委托再来看一下泛型
35、委托,泛型委托支持在返回值和参数上应用类型参数,下面例子中定义了一个类型参数为 T 的委托,然后在类中利用委托调用方法:using System;using System.Collections.Generic;delegate string GetString(T value);class SamplesGenericpublic static string Method1(int i) return i.ToString();public static string Methed2(string s) return s;static void Main()GetString get1 =
36、Method1;string s1 = get1(35);20Console.WriteLine(s1);GetString get2 = new GetString(Methed2);string s2 = get2(“hello“);Console.WriteLine(s2);Console.Read();2.7 泛型集合类在 CLR 2.0 之前,不存在泛型。现在泛型集合类通常是集合的首选类型。泛型集合类是类型安全的,如果使用值类型,是不需要装箱操作的。如果要在集合中添加不同类型的对象,且这些对象不是相互派生的,例如在集合中添加 int 和 string 对象,就只需基于对象的集合类。另
37、一组集合类是专用于特定类型的集合,例如 StringCollection 类专用于 string 类型。对象类型的集合类位于 System.Collections 命名空间;泛型集合类位于System.Collections. Generic 命名空间;专用于特定类型的集合类位于System.Collections.Specialized 命名空间。以下是 System.Collections. Generic 命名空间下的泛型集合类: List 列表 Queue 队列 Stack 栈 LinkedList 链表 HashSet 集 Dictionary 字典 SortedDictionary
38、 SortedList 有序表 Lookup下面以链表为例介绍泛型集合类的应用:using System;using System.Collections.Generic;class SamplesSet20static void Main()LinkedList names = new LinkedList();names.AddLast(“张三“);names.AddLast(“李四“);names.AddLast(“王五“);foreach (string name in names)Console.WriteLine(name);Console.Read();习题与练习一、什么是泛型?泛型有那些优点?二、创建自己的泛型集合类,要求实现 System.Collections.Generic 命名空间下的List 泛型类的核心功能。