c#的程序的编译过程分为两个阶段,第一个阶段将源代码编译为为Microsoft中间语言 (MSIL),JIT编译时,才把c#代码编译为本地可以运行的本地代码,就是经常说的EXE文件或者DLL文件。
当将源代码编译为为MSIL时,对泛型类或者泛型方法编译时,它包含将其标识为具有类型参数的元数据。泛型类型的 MSIL 的使用因所提供的类型参数是值类型还是引用类型而不同。
用值类型指定类型参数来构造泛型类型时,运行库会根据不同的值类型创建专用泛型类型,将提供的参数代入到 MSIL 中的适当位置。对于每个用作参数的不同的值类型,都会创建一次专用泛型类型。
例如,假设您的程序代码声明了一个由整数构造的堆栈,如下所示:
Stack<int> stackInt
那么在此位置,运行库生成Stack <T>类的专用版本,并相应地用整数替换其参数。现在,只要程序代码使用整数堆栈,运行库就会重用生成的专用 Stack<int> 类。
Stack<int> stackOne = new Stack<int>();
Stack<int> stackTwo = new Stack<int>();
这两行代码会重用Stack<int>类而生成该类的两个不同的对象,每个对象本身都是一个独立的个体,只是它们同属于一个类而已。
但是,如果在程序代码中的其他位置创建了另一个 Stack<T> 类,这次使用不同的值类型(如 long 或用户定义的结构)作为其参数,则运行库会生成泛型类型的另一版本(这次将在 MSIL 中的适当位置代入 long)。由于每个专用泛型类本身就包含值类型,因此不再需要转换。
对于引用类型,泛型的工作方式略有不同。第一次使用任何引用类型构造泛型类型时,运行库会创建专用泛型类型,用对象的引用替换 MSIL 中的参数。然后,每次使用引用类型作为参数来实例化构造类型时,无论引用类型的具体类型是什么,运行库都会重用以前创建的泛型类型的专用版本。之所以可以这样,是因为所有引用的大小相同。
例如,假设您有两个引用类型:一个Point 类和一个Line类,并且进一步假设您使用这个泛型类第一次创建了一个Point类型的堆栈:
Stack<Point> StPoint;
在此情况下,运行库生成 Stack<T> 类的一个专用版本,该版本不是存储数据,而是存储稍后将填写的对象引用。假设下一行代码创建另一个引用类型的堆栈,称为 StLine:
Stack<Line> StLine=new Stack<Line>();
不同于值类型,对于Line类型不创建 Stack<T> 类的另一个专用版本。而是创建 Stack<T> 类的一个专用版本的实例,并将Line变量设置为引用它。假设接下来您遇到一行创建Point类型堆栈的代码:
StPoint=new Stack<Point>();
与前面使用Line类型创建的 Stack<T> 类一样,创建了专用 Stack<T> 类的另一个实例,并且其中所包含的指针被设置为引用Point类型大小的内存区域。因为引用类型的数量会随程序的不同而大幅变化,C# 泛型实现将编译器为引用类型的泛型类创建的专用类的数量减小到一个,从而大幅减小代码量的增加。
10.4.7 System.Collections.Generic命名空间与泛型接口
泛型类如前所述,主要应用于集合类上,所以与System.Collections命名空间相对应,为了用户使用的方便,在System.Collections.Generic命名空间提供了很多泛型集合类,而且提供了更多的用于集合的泛型类。
在System.Collections.Generic命名空间提供与System.Collections命名空间功能基本相同的集合类,不是简单的功能上的重复,而主要是泛型集合类避免了以前传统的集合类在装箱/拆箱以及类型不安全的缺点,所以在使用的功能上基本与传统的集合类相同,并且现在的这些集合类是强类型的。
在System.Collections的ArrayList类被List<T>泛型类替代,Stack类被Stack<T>类取代,你基本完全可以在System.Collections.Generic命名空间找到每个原始集合类的对应泛型类。不过也有一些新的泛型类。比如:LinkedList<T>类,就是一个双向链表的泛型类。
双向链表的结构如图10-20所示,在图中用p来表示结点的前结点,用n来表示结点的后结点。下面的箭头用来表明链表的方向。注意在链表中向前其实是指向当前结点的Next方向,如果说向后,那么是指向当前结点的Previous结点。
n |
n |
p |
n |
p |
p |
后 |
结点n+1
|
结点n-1 |
结点n
|
前 |
图10-20
下面就以LinkedList<T>泛型类为例来简单演示一下泛型类的使用。
由于双向链表的每一个结点需要同时记住它前面与后面的结点的位置,所以用于双向链表的结点的结构比较特殊,在其头部与尾部分别需要一个存储相邻结点的数据区域,好在System.Collections.Generic命名空间中已经提供过了这样的节点类LinkedListNode<T>,
图10-21显示了LinkedListNode<T>结点的四个属性。
图10-21
在LinkedList<T>中每个结点的类型都是LinkedListNode<T>,根据每个结点的属性,可以非常容易的得到当前结点的前结点与后结点。由于LinkedListNode<T>类被定义为sealed。所以不能继承该类。
例子LinkedListNodeSample演示了该类的使用方法
namespace LinkedListNodeSample
{
class Program
{
static void Main(string[] args)
{
//创建一个双向链表结点,并填入数据
LinkedListNode<string> StrLinkNode = new LinkedListNode<string>("一班");
PrintProperties(StrLinkNode);
LinkedList<string> ll = new LinkedList<string>();
//将该节点加入到链表中
ll.AddLast(StrLinkNode);
PrintProperties(StrLinkNode);
//在该链表的表首添加一个结点
ll.AddFirst("排头班");
PrintProperties(StrLinkNode);
//在该链表的表尾添加一个结点
ll.AddLast("末尾班");
PrintProperties(StrLinkNode);
Console.ReadKey();
}
/// <summary>
/// 打印结点的属性,即打印该节点所在链表的节点数
/// 如果该节点在链表中,打印出该节点的前向结点与后向结点的值
/// </summary>
/// <param name="lln">LinkedListNode<string></param>
static void PrintProperties(LinkedListNode<string> lln)
{
Console.WriteLine("<------------------------------------->");
//判断这个结点所在的链表
if (lln.List == null)
{
Console.WriteLine("该节点没有被加入链表中");
}
else
{
Console.WriteLine("该节点所在的链表具有{0}个结点",lln.List.Count);
}
//判断该节点的Previous结点
if (lln.Previous == null)
{
Console.WriteLine("该节点的Previous结点为null");
}
else
{
Console.WriteLine("该节点的Previous结点的值是{0}",lln.Previous.Value);
}
//判断该节点的Next结点
if (lln.Next == null)
{
Console.WriteLine("该节点的Next结点为null");
}
else
{
Console.WriteLine("该节点的Next结点的值是{0}", lln.Next.Value);
}
}
}
}
该程序的运行结果如图10-22所示
图10-22
表10-10列出了类LinkedList<T>的属性以及说明
表10-10
属性名称 | 说明 |
Count | 获取 LinkedList<T>中实际包含的节点数。 |
First | 获取 LinkedList<T>的第一个节点。 |
Last | 获取 LinkedList<T>的最后一个节点。 |
表10-11列出了类LinkedList<T>的一些常用方法
表10-11
方法名称 | 功能说明 |
AddAfter | 在LinkedList<T>中的现有节点后添加新的节点或值 |
AddBefore | 在LinkedList<T>中的现有节点前添加新的节点或值 |
AddFirst | 在LinkedList<T>的开头处添加新的节点或值 |
AddLast | 在LinkedList<T>的结尾处添加新的节点或值 |
Contains | 确定某值是否在LinkedList<T>中 |
Find | 查找包含指定值的第一个节点 |
FindLast | 查找包含指定值的最后一个节点 |
Remove | 从LinkedList<T>中移除节点或值的第一个匹配项 |
RemoveFirst | 移除位于LinkedList<T>开头处的节点 |
RemoveLast | 移除位于LinkedList<T>结尾处的节点 |
LinkedList<T>支持枚举数并实现ICollection接口,因为提供了 LinkedListNode<T>类型的单独节点,因此插入和移除的运算复杂度为 O(1)。由于该列表还维护内部计数,因此获取 Count 属性的运算复杂度为 O(1)。而且 LinkedList<T>是双向链表,因此每个节点向前指向Next节点,向后指向Previous节点。如果节点及其值是同时创建的,则包含引用类型的列表性能会更好。LinkedList<T> 接受 null引用作为引用类型的有效Value属性,并且允许重复值。如果 LinkedList<T>为空,则First和Last属性为null。
例子LinkedListSample演示了该泛型类经常使用的一些功能
namespace LinkedListSample
{
class Program
{
static void Main(string[] args)
{
string [] words=new string[]{"A","B","C","D","E"};
LinkedList<string> lls=new LinkedList<string>(words);
PrintLinkList(lls,"原来列表的内容");
lls.AddLast("A");
PrintLinkList(lls, "在列表的最后加A");
lls.AddFirst("G");
PrintLinkList(lls, "在列表的前面加G");
LinkedListNode<string> current = lls.Find("D");
lls.AddBefore(current, "K");
PrintLinkList(lls, "在列表的D前面加K");
lls.AddAfter(current, "H");
PrintLinkList(lls, "在列表的D后面加H");
Console.WriteLine("Y{0}列表中", lls.Contains("Y")?"包含在":"没有在");
Console.WriteLine("G{0}列表中", lls.Contains("G") ? "包含在" : "没有在");
Console.WriteLine("g{0}列表中", lls.Contains("g") ? "包含在" : "没有在");
current = lls.Find("A");
lls.AddBefore(current, "BEFORE");
PrintLinkList(lls, "在第一个匹配的A前插入BEFORE");
current = lls.FindLast("A");
lls.AddAfter(current, "AFTER");
PrintLinkList(lls, "在最后一个匹配的A后插入AFTER");
lls.Remove("BEFORE");
PrintLinkList(lls, "将BEFORE移除后");
lls.RemoveFirst();
PrintLinkList(lls, "将列表的第一个结点移除");
lls.RemoveLast();
PrintLinkList(lls, "将列表的最后一个结点移除");
Console.ReadKey();
}
/// <summary>
/// 打印出列表内容
/// </summary>
/// <param name="ls">需要打印的列表</param>
/// <param name="info">打印的附加信息</param>
static void PrintLinkList(LinkedList<string> ls, string info)
{
Console.WriteLine(info);
foreach (string s in ls)
{
Console.Write(s+" ");
}
Console.WriteLine("/n<----------------------->");
}
}
}
程序的运行结果是:
原来列表的内容
A B C D E
<----------------------->
在列表的最后加A
A B C D E A
<----------------------->
在列表的前面加G
G A B C D E A
<----------------------->
在列表的D前面加K
G A B C K D E A
<----------------------->
在列表的D后面加H
G A B C K D H E A
<----------------------->
Y没有在列表中
G包含在列表中
g没有在列表中
在第一个匹配的A前插入BEFORE
G BEFORE A B C K D H E A
<----------------------->
在最后一个匹配的A后插入AFTER
G BEFORE A B C K D H E A AFTER
<----------------------->
将BEFORE移除后
G A B C K D H E A AFTER
<----------------------->
将列表的第一个结点移除
A B C K D H E A AFTER
<----------------------->
将列表的最后一个结点移除
A B C K D H E A
<----------------------->
在System.Collections.Generic命名空间中提供了7个接口,这些接口也都是泛型化的,即泛型接口。在System.Collections命名空间中如果想实现某个接口,这些接口中的方法的参数都是Object类型的,在方法内一般都需要强制转化为需要的类型,这样就造成了类型不安全。而泛型接口在实现时,可以通过对类型参数的指定,接口内部需要实现的方法的参数就可以直接指定为需要的类型,也就是说,现在不但接口可以是强类型的,而且接口内部的方法也可以是强类型的。
接口IEqualityComparer<T>主要是用来提供判断两个对象是否相等的功能。如果在集合中需要判断两个对象是否相等,那么集合内的元素对象就应该具有这个方法。否则,将会出错。程序会提示你,集合中的对象没有提供判断相等的方法。
接口IEqualityComparer<T>内部有两个方法需要实现,它们分别是Equals与GetHashCode,可能很多人不明白为什么还有一个GetHashCode方法,而不仅仅是只有一个Equals方法,这里GetHashCode方法有什么用呢?下面会给大家演示这个方法的作用。
提供判断相等方法的途径有两种:
第一种:直接把判断两个对象相等的函数作为类的方法,这个方法可以作为自定义类的一个普通的成员方法,或者该类就实现IEqualityComparer<T>接口。集合在需要判断相等时,会自动调用元素对象的该方法。
第二种:有些泛型集合的构造函数的参数中,就有一个是用来提供判断相等功能的对象,在需要判断元素对象是否相等时,会自动调用从参数传入的对象提供的相等方法。
泛型类Dictionary<TKey, TValue>在判断键是否相等时,就可以使用第二种途径来实现。例子GenericsDictionary就使用了一个自定义的MyEquals类,该类在内部重写了IEqualityComparer<T>接口的两个方法。不过演示的例子并没有直接实现IEqualityComparer<T>接口,而是从EqualityComparer<T>抽象基类继承下来。这是因为在System.Collections.Generic命名空间又提供了一个实现IEqualityComparer<T>接口的抽象基类EqualityComparer<T>。
例子GenericsDictionary中MyEquals类的代码:
namespace GenericsDictionary
{
class MyEquals:EqualityComparer<string>
{
/// <summary>
/// 重写Equals方法
/// </summary>
/// <param name="x">string</param>
/// <param name="y">string</param>
/// <returns>如果相等返回true,否则返回bool</returns>
public override bool Equals(string x, string y)
{
Console.WriteLine("我是自定义的Equals");
return string.Equals(x, y);
}
/// <summary>
/// 重写GetHashCode
/// </summary>
/// <param name="s">string</param>
/// <returns>返回字符串的哈希码</returns>
public override int GetHashCode(string s)
{
Console.WriteLine(s);
return s.GetHashCode();
}
}
}
测试代码:
namespace GenericsDictionary
{
class Program
{
static void Main(string[] args)
{
MyEquals myequals = new MyEquals();
Dictionary<string, int> dict = new Dictionary<string, int>(myequals);
dict.Add("A", 1);
dict.Add("B", 2);
dict.Add("b", 2);
Console.WriteLine(dict.ContainsKey("A"));
Console.ReadKey();
}
}
}
程序运行的结果
A
B
b
A
我是自定义的Equals
True
前面三行的结果A,B,b是因为调用GetHashCode方法打印出来的,第四行A也是调用GetHashCode方法打印出来的,“我是自定义的Equals”这句话是因为调用Equals方法打印出来,最后返回结果True。从这里可以看到在运行ContainsKey方法时,首先调用了GetHashCode方法,而不是直接调用Equals方法。为什么?
把上面代码中加边框的代码换为
Console.WriteLine(dict.ContainsKey("a"));
再运行一下程序,输出的结果为:
A
B
b
a
False
看到并没有打印出“我是自定义的Equals”这句话,看来Equals方法并没有被调用,原来ContainsKey方法的运行过程是这样的:代码首先调用GetHashCode方法,如果得到的哈希码与前面的哈希码有相同的,接着调用Equals方法,再判断两个对象是否相同。如果相同,返回True,否则返回False,如果得到的哈希码与前面的哈希码都不相同,那么这个对象一定不在集合中,直接返回False。程序的这个运行特点是由哈希函数的特点决定的。