注意:C# 2.0大纲根据网络资料收集整理而成,并在各个部分加以个人的理解以及运用。本文中的内容并不能够达到完整的层次,但是覆盖最基本的要求,并且往往可以通过本文的内容举一反三使得自己提升到更高的层次。如果有不足之处,恳请指点。
C# 2.0需要支持.NET Framework 2.0的编译器,对于MS来说就是VS 2005,开源社区的SharpDevelop2也是不错的选择,但是Borland最新的C# Builder 2006暂不支持.NET 2.0。在这里列举这些特性的目的就是让各位已经开始使用VS 2005等开发工具的朋友可以在短时间熟悉新语言环境,运用新的语言特性,高效率完成任务。
C# 2.0引入了很多语言扩展,最重要的就是泛型(Generics)、匿名方法(Anonymous Methods)、迭代器(Iterators)和不完全类型(Partial Types)。
1. 泛型允许类、结构、接口、委托和方法通过它们所存贮和操作的数据的类型来参数化。泛型是很有用的,因为它提供了更为强大的编译期间类型检查,需要更少的数据类型之间的显式转换,并且减少了对装箱操作的需要和运行时的类型检查。
2. 匿名方法允许在需要委托值时能够以“内联(in-line)”的方式书写代码块。匿名方法与Lisp语言中的拉姆达函数(lambda functions)类似。
3. 迭代器是能够增量地计算和产生一系列值得方法。迭代器使得一个类能够很容易地解释foreach语句将如何迭代他的每一个元素。
4. 不完全类型允许类、结构和接口被分成多个小块儿并存贮在不同的源文件中使其容易开发和维护。另外,不完全类型可以分离机器产生的代码和用户书写的部分,这使得用工具来加强产生的代码变得容易。
不过呢,虽然具有这些改进(有的需要从.NET内部进行革新),但是在源代码级别将保持高度的兼容性,所以完全可以放心遗留代码可以在新的环境中通过编译(不过不能保证能否良好运行)。
下面,我将分别阐述这4种新特性。
一、泛型
在Ada实现泛型、C++实现Template之后,Java在编译器层次实现泛型,C#也在.NET底部提供泛型支持。可以预见,语言动态化是一种趋势,泛型也将为C#的更加灵活提供支持与动力。
为什么要利用泛型呢?曾经我们一直都在设想拥有一个通用数组,可以存放任意类型的东西。在以前,我们可以用object[]或者ArrayList来实现。但是这样的话,我们在取出里面的元素时,都需要进行貌似无所谓的类型错误检查,如下例:
ArrayList list = new ArrayList();
myClass cls1 = new myClass();
list.Add(cls1);
myClass cls2 = (myClass)list[0]; //要进行类型转换
而当具体类型为int等值类型时就更不妙了,存入需要进行装箱,取出还要拆箱,这样不仅需要在装箱、拆箱耗费时间来进行内存分配,还要进行类型错误检查。
此外,这种方法还不能强制数据的种类。比如,上例中,如果取出时
string str = (string)list[0];
这样在编译的时候不会出现任何问题,直到运行之后才会出现InvalidCastException异常。这对于日常的编程调试是十分不利的。而泛型可以杜绝这种问题的出现。
那么我们现在看一下一个典型的泛型类(List是ArrayList的等效泛型类)的构成:
public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>, IList, ICollection, IEnumerable{
public void Add (T item){...}
public T this [int index] { get{...} set{...} }
...
}
这里只举出一个Add方法和一个索引属性,不过足够说明问题。
那么,我们用List<int>初始化之后,List<T>中的所有类型参数T都被实际的类型名int所替换。相当于变成:
public class List<int> : IList<int>, ICollection<int>, IEnumerable<int>, IList, ICollection, IEnumerable{
public void Add (intitem){...}
public int this [int index] { get{...} set{...} }
...
}
这样定义一个泛型类List<T>,就可以使用近乎于无数个类似的构造类型(每个构造类型都是相互独立的)。比如,List<int>在相关部分只处理int,而List<string>则专门处理string类型。不会有object转换,也更不会有装箱、拆箱,无效的类型转换也会在编译时检测到。
List<int> lis t= new List<int>();
list.Add(123);
int a = list[0]; //无需拆箱等多余操作
myClass cls = list[0]; //错误,类型不匹配
现在需要讲一下替代T的实际类型的概念,实际类型其实要求非常低,只要是真实存在的符合T的约束条件(约束在后面会讲到)的可用类型即可。在符合约束条件的前提下,如int,Control,甚至是List<int>,int[][]等均不成问题。所以,泛型带来的便利今后将深刻地体验到。
一个泛型类可以有多个类型参数,比如
public class GenericSample<T1, T2, T3, T4>{...}
在使用构造类型时,自然要全部给与适当的实际类型。比如GenericSample<int, string, long, char>等。当然,系统不在乎你是否赋予同样的实际类型。即使用GenericSample<int, int, int, int>,只要符合你的需要,自然是合理有效的。
前面已经讲过,构造类型就是一个独立的类型了。所以在使用它的时候只要完整运用即可。比如List<int> list=new List<int>();中List<int>就作为一个整体来看待,所以构造类型的基本运用应该就比较清楚了。相信这一点是需要大家明确的,不然闹出像“List()<int>”这样的笑话是挺麻烦的。
也许在某些情况下,我们并不关心整个类的改变类型重用,而是认为一个方法即可满足要求,这时候我们就可以使用泛型方法。
比如下例中,ConCat函数将两个变量分别调用ToString()的返回值进行连接,然后返回连接结果:
public string ConCat<T1, T2>(T1 var1, T2 var2){
return var1.ToString() + var2.ToString();
}
这样,就实现了ConCat对于任何类型的复用。
同样的,我们也可以按照这样的思路实现泛型委托。
相信大家在编写事件会发现,参数表往往是(object sender, EventArgs e)之类的形式,而e的类型有时是EventArgs,有时是MouseEventArgs等等。那么,我们在编写委托的时候就需要变更一个类型就编写一个新的委托,但是这样的意义并不大。
.NET 2.0中提供了一个EventHandler泛型委托,定义如下:
public delegate void EventHandler<TEventArgs> (Object sender, TEventArgs e) where TEventArgs : EventArgs
大家暂时不要去理会where子句的意义,其它部分的内容相信很快就能了解了吧。
利用这个EventHandler,就能够通过实现不同的构造类型来完成新的事件的定义,而无需重新编写一个委托。(不过呢,MS在写.NET 2.0的Forms时,并没有采用这种方式,仍然按照以前的方法写了一堆委托,估计是出于兼容遗留代码的考虑)
下例就是利用EventHandler实现MouseClick的方法:
public delegate void MouseEventHandler (Object sender, MouseEventArgs e);
public event MouseEventHandler MouseClick;
//上为.NET中的实现方法
//下为利用泛型委托的实现方法
public event EventHandler<MouseEventArgs> MouseClick;
此外,我们往往要对泛型类型参数进行约束,因为并非所有的类型都能够合法完成本泛型类的操作。在这里,我们就需要利用where上下文关键字对类型参数进行约束。
比如,如果设计一个函数
SetControlEnabled<TControl>(TControl ctrl, bool enabled){
ctrl.Enabled = enabled;
}
用来设定控件的Enabled属性。这时,并不是所有类型都可以代替TControl,当string代入时,显然会由于缺少Enabled属性而出错。
所以,只要约束TControl的实际类型必须继承自Control即可:
SetControlEnabled<TControl>(TControl ctrl, bool enabled) where TControl : Control {
ctrl.Enabled = enabled;
}
编译器会对不符合要求的类型代入发出错误警告。
这种约束不仅可以运用在泛型方法中,也可以在泛型类中运用,自然还可以在泛型委托中运用(如前面提到的EventHandler泛型委托)。
另外,每个类型参数都可以设定各自的约束(只设定部分参数自然也可以),不过如果要设定多个类型参数,那么每个类型参数都需要对应一个where子句。
以下,将对泛型的具体运用作一下基本的补充:
1.由于泛型类也具有普通类的效力,所以在共同作用域中,不要定义重名的类(GSample和GSample<T>以及GSample<T, K>均属于重名现象)。而对于方法、委托而言,有可能通过代入实际类型而导致签名一致的方法均无效,如(T1 param1, T2 param2)和(int a, int b)有可能签名相同(当然,这里的“一致”、“相同”建立在非泛型的合法方法重载基本理论之上,譬如ref与out视为等价签名)。方法、委托的重名规则同样适用于继承用的基类和基接口,如I<T>和I<K>重名。
2.类型参数不能作为基类或者基接口,如class GSample<T> : T {}就是错误的。
3.类的类型参数不得与类或者成员的名称相同。
4.类的类型参数也有其作用域,覆盖类体、约束子句、基类部分以及嵌套类型,但不覆盖到其派生类。在嵌套类型中的作用效果等同于变量在类与嵌套类中的作用效果。
5.前面已经提到,一个泛型类的构造类型是相互独立的,所以泛型类各个构造类型的静态组成部分(如静态构造函数、静态成员等)均是相互独立的。
6.应用程序的入口点函数不能处于一个泛型类中。
二、匿名方法
其实匿名方法说起来也很简单,就是一种不需要名称的内联委托。
以前,我们要写下面这些代码:
public class Sample : Form{
Button btn;
public Sample{
btn = new Button();
btn.Click += new EventHandler(btn_Click);
}
private void btn_Click(object sender, EventArgs e){
this.Close():
}
}
在上例中,为了一个简单的this.Close;而新建一个带有两个参数的方法,而当我们运用匿名方法之后,会变得很简单:
public class Sample : Form{
Button btn;
public Sample{
btn = new Button();
btn.Click += delegate{
this.Close();
};
}
}
这样我们可以看到,匿名方法适用于这种不想浪费一个单独方法来完成一项简单操作的情形。同样我们也可以感觉到,匿名方法其实用起来也很简单。
一个匿名方法包括关键字delegate以及一个可选的参数表。在上例中,我已经将参数表省略了,然而如果遇到方法中需要访问参数的情形,则必须提供完整的参数表:
btn.Click += delegate(object sender, EventArgs e){
((Button)sender).Enabled = false;
};
当然,方法设计也可以如此:
delegate void DelegateSample();
static void Main(){
DelegateSample d = delegate{
Console.WriteLine("Hello");
}
d();
Console.WriteLine("Again...");
d();
}
这样,就无需单独新建一个方法却可以反复重用这一部分代码。
怎样设计才是一个合法的匿名方法呢?需要满足两点:
1.参数表匹配。
2.匿名方法的返回值可以显示转换为目标委托的返回值,或者均无返回值。
此外,C# 2.0还提供方法的自动转换(方法组转换),比如最开始的例子中:
btn.Click += new EventHandler(btn_Click);
其实可以直接写为:
btn.Click += btn_Click;
编译器会自动进行判断。
三、迭代器
我想大家都应该用过foreach吧。其实foreach就是一种迭代的调用。在这里,我们将讨论一个基本迭代的组成。
为了实现枚举,一个可枚举的(enumerable)的集合要有一个无参的、返回枚举器(enumerator)的GetEnumerator方法。
而要获得枚举器,我们就需要一个迭代器。所谓迭代器,就是一个可以产生有序的值序列的语句块。这里,我们就需要引入yield上下文关键字。其中,yield return产生本次迭代的值,yield break指示本次迭代完成。
创建迭代器一般有两种方法:
1.对可枚举接口(即IEnumerable和IEnumerable<T>)实现GetEnumerator方法,然后对该类直接使用foreach迭代。
2.创建一个返回可枚举接口(即IEnumerable和IEnumerable<T>)的函数,对该函数的返回值使用foreach迭代。
通过这两种方式,访问数据集合。
以下,将分别举例阐述:
1.实现GetEnumerator方法。
public class ListSample<T> : IEnumrable<T>{
private T[] items;
...//some methods to modify items
public IEnumerator<T> GetEnumerator(){
for(int i=0; i<items.Length; i++){
yield return items[i];
}
}
}
这样便完成了迭代器,然后便可以通过foreach语句迭代该类返回items中的所有元素。
ListSample<int> list = new ListSample<int>();
foreach(int i in list){
Console.WriteLine(i);
}
2.创建迭代函数
public class ListSample<T>{
private T[] items;
...//some methods to modify items
public IEnumerable<T> Iterator(){
for(int i=0; i<items.Length; i++){
yield return items[i];
}
}
}
这样,我们就可以通过Iterator的返回值进行迭代。
ListSample<int> list = new ListSample<int>();
foreach(int i in list.Iterator()){
Console.WriteLine(i);
}
第一种方法的优点就是设计方便简单,直接实现IEnumerable或者IEnumerable<T>即可;第二种方法的自由度大,可以自行为函数设定参数表,来自定义迭代的范围等等。
然后说一下基本注意事项吧:
1.yield语句不能用于匿名方法,yield语句不能用于finally块中,yield return不能用于try...catch块中。
2.迭代函数的参数中不能出现ref或者out。
其它的内容,诸如MoveNext()等方法的具体实现以及异常处理块中的情况讨论等问题由于较为复杂且平时不常用到,这里暂不讨论。有兴趣可以参考微软相关文献。
四、不完全类型
C#允许在一个代码文件中存放多个类,但这样往往不便于类的管理,所以一向是提倡一个文件中只存放一个类。不过呢,随着类规模的不断膨胀,一个文件中存放一个类也有些显得臃肿,或者是在某个角度上不便于代码的组织。
因此,C# 2.0中引入了不完全类型的概念,即启用了新的修饰符partial。借助该修饰符,我们可以在多个文件中存放一个类,每个文件只包含该类的某些功能。当然,它并不单单可以修饰class,还可以修饰struct和interface。
正如VS 2005对Windows窗体代码的组织那样,界面部分代码被单独存放在一个文件中,其它代码存在另外一个文件中,这样正符合平时大多只关注非界面代码的现实。
由于这一部分涉及的内容比较少,就无需展开来讲,下面将阐述一下注意事项:
1.partial必须直接位于class,struct,interface之前。
2.不可用partial扩展已经编译的类型。
3.运用partial之后,对类型的任何编辑(成员、特性、修饰符、基类、约束等)将合并。
4.运用partial的类型要一块进行编译。
5.运用partial的类型必须处于同一命名空间之中。
补充:Nullable
.NET 2.0中提供了Nullable泛型结构,Nullable类。
C# 2.0也从语法上支持Nullable。
这里只讨论Nullable泛型结构,因为Nullable类是Nullable泛型结构的补充。
我们知道,引用类型可以被赋予空值(null),而值类型不可。但是利用Nullable之后,值类型也可以这么做。运用Nullable很简单,只需在声明类型后面加上一个问号即可。如
private int? sample;
然后在操作时只需要注意两个属性:一个是HasValue,用于指示当前是否存在值;一个是Value,如果存在值则返回值,否则抛出异常。
C#也有替代的方案,那就是??运算符。
Console.WriteLine("sample is {0}", sample ?? "NULL");
??运算符检查左边的Nullable是否含有值,如果则返回值,否则返回运算符右边的操作数的值。