这篇文章是翻译的微软的技术文章.供学习c#的朋友参考,请勿用于商业目的。http://msdn.microsoft.com/vcsharp/team/language/default.aspx
19.C#2.0介绍
C#2.0引入了几项语言扩展,其中最重要的是泛型、匿名方法、迭代器和不完整类型(partial type)。
泛型可以让类、结构、接口、委托和方法,通过他们所存储和操纵的数据的类型被参数化。泛型是很有用的,因为他们提供了更强的编译时类型检查,减少了数据类型之间的显式转换,以及装箱操作和运行时类型检查。
匿名方法可以让代码块以内联的方式潜入到期望委托值的地方。匿名方法与Lisp 编程语言中的λ函数(lambda function)相似。C#2.0支持“closures”的创建,在其中匿名方法可以访问相关局部变量和参数。
迭代器是可以递增计算和产生值的方法。迭代器让类型指定foreach语句如何迭代它的所有元素,变得很容易。
不完整类型可以让类、结构和接口被拆分成多个部分存储在不同的源文件中,这更利于开发和维护。此外,不完整类型允许某些类型的机器生成的部分与用户编写的部分之间的分离,因此增加由工具产生的代码很容易。
本章将介绍这些新特征。介绍完之后,接下来的四章提供了这些特征的完整的技术规范。
C#2.0的语言扩展主要被设计用于确保与现存的代码之间最大的兼容性。例如,尽管C#2.0对于where、yield 和partial这些词在特定上下文中赋予了特别的意义,但这些词仍然可被用作标识符。实际上,C# 2.0没有增加任何可能与现有代码中的标识符冲突的关键字。
19.1 泛型
泛型可以让类、结构、接口、委托和方法,通过他们所存储和操纵的数据的类型被参数化。C#泛型对于使用Eiffel或Ada的泛型的用户,或者对于C++模板的用户来说是很熟悉的;但他们将不用再去忍受后者的众多的复杂性。
19.1.1为什么使用泛型
没有泛型的话,通用目的的数据结构可以采用object类型存储任何类型的数据。例如,下面的Stack类在一个object数组中存储数据,而它的两个方法,Push和Pop相应地使用object接收和返回数据。
public class Stack
{
object[] items;
int count;
public void Push(object item){…}
public object Pop(){…}
}
尽管使用类型object可以使得Stack类更加灵活,但这样做也并不是没有缺点。例如,你可以将一个任何类型的值,诸如,Customer的一个实例压入(Push)堆栈。但当你取回一个值时,Pop方法的结果必须被显式地强制转换到合适的类型,为一个运行时类型检查去编写代码,以及带来的性能不利影响,是很令人讨厌的。
Stack stack = new Stack();
Stack.Push(new Customer());
Customer c = (Customer)stack.Pop();
如果一个值类型的值,例如一个int被传递到Push方法,它将会被自动装箱。当后面获得这个int 时,它必须使用一个显式的强制转换而被取消装箱。
Stack stack = new Stack();
Stack.Push(3);
int I = (int)stack.Pop();
这种装箱和取消装操作增加了性能开销,因为它们涉及到动态内存的分配和运行时类型检查。
Stack类的更大的问题是,它不能强制放置在堆栈上的数据种类。实际上,Customer实例可以被压入堆栈,而取回它时可能被强制转换到错误的类型。
Stack stack = new Stack();
Stack.Push(new Customer());
String s = (string)stack.Pop();
尽管先前的代码是Stack类的一种不恰当用法,但这段代码从技术上说是正确的,并且也不会报告编译时错误。问题直到代码执行时才会冒出来,在这一点上将会抛出一个InvalidCastException异常。
如果Stack类具有能够指定其元素的类型能力,那么很显然它能从这种能力得到好处。使用泛型,这将会变成可能。
19.1.2 创建和使用泛型
泛型为创建具有类型参数(type parameter)的类型提供了工具。下面的例子声明了一个带有类型参数T的泛型Stack类。类型参数在类名字之后的“<“和“>”分界符中指定。这里没有object与别的类型之间的相互转换,Stack<T>的实例接受它们被创建时的类型,并且存储那个类型的数据而没有转换它。类型参数T充当一个占位符,直到使用的时候才指定一个实际的类型。注意,T被用作内部items数组的元素类型、Push方法参数的类型和Pop方法的返回值类型。
Public class Stack<T>
{
T[] items;
int count;
public void Push(T item){…}
public T Pop(){…}
}
当泛型类Stack<T>被使用时,T所代替的实际类型将被指定。在下面的例子中,int 将被作为T的类型参数而给出。
Stack<int> stack = new Stack<int>();
Stack.Push(3);
int x = stack.Pop();
Stack<int>类型被称为构造类型(constructed type)。在Stack<int>类型中,T的每次出现都被使用类型参数int代替。当Stack<int>的实例被创建时,items数组的本地存储就是一个int[]而不是object[],与非泛型Stack相比,它提供了更高的存储效率。同样地,在int值上的Stack<int>操作的Push和Pop方法,将会使得压入其他类型的值到堆栈中出现一个编译时错误,并且当取回值的时候也不需要转换回它们原始的类型。
泛型提供了强类型,意义例如压入一个int到Customer对象堆栈将会出现错误。就好像Stack<int>被限制只能在int值上操作,同样Stack<Customer>也被限制用于Customer对象。
对于下面的例子,编译器将会在最后两行报告错误。
Stack<Customer> stack = new Stack<Customer>();
Stack.Push(new Customer());
Customer c = stack.Pop();
stack.Push(3); //类型不匹配错误
int x = stack.Pop(); //类型不匹配错误
泛型类型声明可以有任意数量的类型参数。先前的Stack<T>例子 只有一个类型参数,但一个通用的Dictionary类可能有两个类型参数,一个用于键(key)的类型,另一个用于值(value)的类型。
public class Dictionary<K , V>
{
public void Add(K key , V value){…}
public V this[K key]{…}
}
当Dictionary<K , V> 被使用时,必须提供两个类型参数。
Dictionary<string , Customer> dict = new Dictionary<string , Customer>();
Dict.Add(“Peter”, new Customer());
Custeomer c = dict[“Perter”];
19.1.3泛型类型实例化
与非泛型类型相似,被编译过的泛型类型也是由中间语言[Intermediate Language(IL)]指令和元数据表示。泛型类型的表示当然也对类型参数的存在和使用进行了编码。
当应用程序首次创建一个构造泛型类型的实例时,例如,Stack<int>,.NET公共语言运行时的实时编译器(JIT)将在进程中把泛型IL和元数据转换为本地代码,并且将类型参数替换为实际的类型。对于那个构造泛型类型的后续引用将会使用相同的本机代码。从一个泛型类型创建一个特定构造类型的过程,称为泛型类型实例化(generic type instantiation)。
.NET公共语言运行时使用值类型为每个泛型类型实例创建了一个本地代码的特定拷贝,但对于所有的引用类型它将共享那份本地代码的单一拷贝(因为,在本地代码级别,引用只是带有相同表示的指针)。
19.1.4约束
一般来讲,泛型类不限于只是根据类型参数存储值。泛型类经常可能在给定类型参数的类型的对象上调用方法。例如,Dictionary<K , V>类中的Add方法可能需要使用CompareTo方法比较键值。
public class Dictionary<K , V>
{
public void Add(K key , V value)
{
…
if(key.CompareTo(x)<0){…}//错误,没有CompareTo方法
…
}
}
因为为K所指定的类型参数可能是任何类型,可以假定key参数存在的唯一成员,就是那些被声明为object类型的,例如,Equals,GetHashCode和ToString;因此,在先前例子中将会出现编译时错误。当然,你可以将key参数强制转换到一个包含CompareTo方法的类型。例如,key参数可能被强制转换到IComparable接口。
public class Dictionary<K , V>
{
public void Add(K key , V value)
{
…
if(((IComparable)key).CompareTo(x)<0){…}
…
}
}
尽管这种解决办法有效,但它需要在运行时的动态类型检查,这也增加了开销。更糟糕的是,它将错误报告推迟到了运行时,如果键(key)没有实现IComparable接口将会抛出InvalidCastException异常。
为了提供更强的编译时类型检查,并减少类型强制转换,C#允许为每个类型参数提供一个约束(constraint)的可选的列表。类型参数约束指定了类型必须履行的一种需求,其目的是为了为类型参数被用作实参(argument)。约束使用单词where声明,随后是类型参数的名字,接着是类或接口类型的列表,和可选的构造函数约束new()。
public class Dictionary<K, V> where K :IComparable
{
public void Add(K key , V value)
{
…
if(key.CompareTo(x)<0){…}
…
}
}
给定这个声明,编译器将会确保K的任何类型实参是实现了IComparable接口的类型。
并且,在调用CompareTo方法之前也不再需要对key参数进行显式地强制转换。为类型参数作为一个约束而给出的类型的所有成员,对于类型参数类型的值时直接有效的。
对于一个给定的类型参数,你可以指定任意数量的接口作为约束,但只能有一个类。每个约束的类型参数有一个单独的where 语句。在下面的例子中,类型参数K有两个接口约束,类型参数e有一个类约束和一个构造函数约束。
public class EntityTable<K, E>
where K:IComparable<K>,IPersisable
where E:Entity, new()
{
public void Add(K key , E entity)
{
…
if(key.CompareTo(x)<0){…}
…
}
}
在前面的例子中,构造函数约束new(),确保为E用作类型参数的类型具有一个公有的、无参数构造函数,并且它允许泛型类使用new E()创建该类型的实例。
类型参数约束应该很小心的使用。尽管它们提供了更强的编译时类型检查,在某些情况下增强了性能,但它们也限制了泛型类型的可能的用法。例如,泛型类List<T>可能约束T实现IComparable接口,由此它的Sort方法将可以比较项的大小。然而,这么做却使得没有实现IComparable 接口的类型不能使用List<T>,即使是在这些情形下,Sort方法根本就没有被调用过。