.NET之我见系列 - 类型系统(下)
1,泛型
泛型在.NET中的重要价值已经无需用过多的语言来描述了。从.NET2.0提出泛型开始,这个东西已经被开发人员广为称道,颇有相见恨晚之意。可以想象得到,.NET在设计之初就想实现这种特性。微软把这一希望寄托在了System.Object上,但事后证明,后者所带来的性能开销是开发人员所无法接受的。因此纷纷弃用这一鸡肋。为了解决这一问题,在.NET2.0上,微软全力攻关,终于搞出了一个完美的解决方案,就是泛型。实际上,泛型是一种虚拟意义上的“泛型”,它的原理也很容易理解,就是在第一次编译时,使用一个占位符代替原来应使用类型的位置,当第二此编译时再用代码中指定的类型替换这个占位符。记得以前大学里上课时,经常有的寝室只派一个人来抢座位,把书或包放在空位置上,就表示这个位置已经有人,其他人不能再坐。等到上课前一分钟,这些人才睡眼惺惺地跑来抢现成的座位。大概想出泛型的这位老大也是受了大学里的启发(纯属猜测)。
这一过程用代码这样表示:
源程序中的代码:
{
public void Say(T word)
{
Console.WriteLine(word.ToString());
}
}
这个泛型类表示了一个人类的类别,在类名Human后用<T>表示泛型的引入,其中T就是一个占位符,在此处,它可以表示任何的类型。而在Say方法中,T作为参数的类型,从而用于接收不同类型的值。
当第一编译结束后,以上程序的IL代码变成如下所示:
图中我们可以看到,无论是在Human类的后面和Say方法的参数中,都使用T来作为类型标识,当然T只是安装微软的规范,这里你完全可以用另一个字符或单词来表示,效果是同样的。但此时,类和方法还并不清楚自己是什么类型。直到调用该类的Main方法中出现下面的代码:
此时,通过智能感知提示,我们已经很清楚的看到,Say方法后的参数类型被指定为string型。表明泛型类在初始化时,指明了类型为string,并用string替换了占位符T。实际上这一转换发生在二次编译之时,也就是JIT编译时。由此我们可以得知,.NET的泛型是一种虚拟意义的泛型,只是利用了一个占位符为中介,让程序达到了泛型的效果。这也是一种较完美的解决方案。
顺便提一下,泛型的实现原理在王涛先生的《你必须知道的.NET》一书中第10章里讲得很清楚了,对这一概念仍不大清楚的朋友还可以去好好学习一下。
2.实现
了解泛型的原理,再来使用泛型也就不是那么难的事了。我们首先要知道的是,泛型给我们带来的最大的好处就是解决了类型转换所引起的性能损失,也避免了这种转换所引起的各种错误。因此我们就明白了,在经常发生类型的转换的地方就需要使用泛型。那么哪些地方经常会发生类型转换啦,当然就数集合了。我们看如下示例:
using System.Collections;
namespace ConsoleApplication5
{
class Program
{
static void Main(string[] args)
{
ArrayList al = new ArrayList();
al.Add("abc");
al.Add(123);
al.Add('a');
al.Add(true);
foreach (string str in al)
{
Console.WriteLine(str.ToString());
}
}
}
}
这个示例中使用了集合类ArrayList,由于未规范类型的使用,导致此程序中在向集合内添加记录时,发生了3次装箱操作,将极大降低程序的性能。这还不算什么,关键是最后使用foreach循环时,由于集合类中的数据类型各异,导致在读取记录时发生类型转换的错误,但此错误在编译期间却无法检测出来,原因是没有安全的使用集合类。
当然,我们实际开发中很少这样SB地使用集合类,但一个优秀的程序员总是希望能用最安全最稳妥的办法来消除一切可能存在的隐患,决不允许有这样的不稳定因素遗留在程序中,因此,.NET在System.Collections.Generic这个命名空间中为我们提供了泛型的集合类实现。
using System.Collections;
using System.Collections.Generic;
namespace ConsoleApplication5
{
class Program
{
static void Main(string[] args)
{
List<int> list = new List<int>();
list.Add("abc"); //此处是错误的添加集合数据,程序将不能通过编译检查。
list.Add(123);
}
}
}
再调试或编译这段程序就会报错,原因是使用了泛型的List集合,在定义之初就锁定了类型为int型。若添加集合数据时不匹配的操作,则程序无法编译通过检查。也就避免了可能出现的问题。
3.泛型约束
就这上面的话题,顺便讲到做开发的一个原则:“错误要尽量早的发现”。要将一切可能出现的错误扼杀在摇篮中,用一句经典的反派台词来说就是”另可错杀一千,不可放过一个“。(做程序员难道都注定要变成冷血动物么?)
那么要遵循这条原则,就要给泛型加上约束了。约束是对泛型类更进一步的规范。看下面这个例子:
这个例子中 ,英雄类是这样定义的:class Hero<T> where T : Weapon, new() ,表明英雄类定义了一个泛型参数T,用于装备武器的AddArm方法。但为这个参数定义了两个约束,一个是必须为Weapon接口的实现,一个是必须有无参的构造函数。当然你还可以为英雄定义更多的武器,但前提是必须实现Weapon接口。该程序的运行结果如下:
除了上面所讲到的约束外,泛型约束还有以下几种:
T:结构 |
类型参数必须是值类型。可以指定除 Nullable 以外的任何值类型. |
T:类 |
类型参数必须是引用类型,包括任何类、接口、委托或数组类型。 |
T:new() |
类型参数必须具有无参数的公共构造函数。当与其他约束一起使用时,new() 约束必须最后指定。 |
T:<基类名> |
类型参数必须是指定的基类或派生自指定的基类。 |
T:<接口名称> |
类型参数必须是指定的接口或实现指定的接口。可以指定多个接口约束。约束接口也可以是泛型的。 |
T:U |
为 T 提供的类型参数必须是为 U 提供的参数或派生自为 U 提供的参数。这称为裸类型约束。 |
这些泛型约束在使用上可配合使用,但也有一定的限制 ,比如T:class 不可和T:struct合用,这个很容易理解,因为一个是值类型,一个是引用类型嘛。还有一些其它的限制,我们这里不一一列举了,在实践中一试即知。本文主要的目的就是要弄懂原理。
最后总结一下。泛型其实并不是一个很难理解的东西,关键是弄清楚它的设计目的,就是为了解决类型转换所带来的不安全因素和性能损失的问题,而原理就是在进行二次编译之前用一个占位符替换类型的定义,再由JIT将占位符一一替换成指定的类型,最后编译。所以我称之为伪泛型!
讲到这里突然又想到一点,VS2008中搞了一个隐式类型var,这个能不能看成真正的泛型啦?大家给点意见吧!