第三章 泛型
和Java、C++一样,区别于Python、Javascript、Ruby等语言,C#是强类型语言。所谓强类型,就是指编译器会在编译阶段检查类型和对象调用的合法性。
在第一章,我们举了一个例子:
1
2
|
int i = 1; i.CompareTo(2); |
这个代码是合法的,但是
1
2
|
object i = 1; i.CompareTo(2); |
这样的代码是非法的。因为编译器会检查i的类型,它是object的,而object类型并没有CompareTo这个方法。有人说了,为什么编译器不能“智能一点”,根据i=1推断出i就是int类型呢。
那你冤枉编译器了,有时候,这真不好推断:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
static bool IsInteger( string s) { int n = 0; return int .TryParse(s, out n); } static void Main( string [] args) { string s = Console.ReadLine(); object i; if (IsInteger(s)) i = int .Parse(s); else i = s; i.CompareTo(2); } |
我们的程序从控制台读入一个字符串,如果它表示一个整数,我们就让i以整数的方式接收它,如果是别的字符串,我们就以字符串的形式接收它。我们前面说了,i被定义为object类型,那么它天然地可以接受任意类型。
此时编译器怎么知道i的类型究竟是整数还是字符串——我们也不知道,除非程序运行,用户输入了以后才知道。
总而言之,编译器在编译的时候必须确定一个变量的类型,因为它会把它和调用它的代码硬编码到可执行文件中去,所以它不得不确认这一点。
编译器不但会检查一个变量是否能调用某个方法,如上面所述的那样,还会检查某个对象能不能传给这个变量,比如
1
2
3
4
|
int i = 0; object o = 1; i = o; // error o = i; |
编译器只允许object类型接收int类型的变量(int是object的派生类型),但是决不允许int类型接收object类型的变量。因为object类型不但可以表示int,也可以表示别的类型,它们和int类型并不兼容。而一个object类型究竟表示什么具体的类型,我们有时候还是得等运行的时候才知道。编译器不敢乱猜。
我们来编写一个函数,比较两个数是否相等:
同学甲不假思索编写了如下代码:
1
2
3
4
|
static bool IsEqual( int a, int b) { return a == b; } |
同学乙马上反驳道,你怎么知道是整数?如果是浮点数呢?
甲同学说,这个好办,我再写一个就是了:
1
2
3
4
5
6
7
8
|
static bool IsIntEqual( int a, int b) { return a == b; } static bool IsFloatEqual( float a, float b) { return a == b; } |
但是马上意识到,这样写很呆,不过他马上就想到一个“好办法”:
1
2
3
4
|
static bool IsEqual( object a, object b) { return (a as IComparable).CompareTo(b) == 0; } |
使用object类型代替了具体的类型,这下不管传入什么,编译器都统统放行。
乙同学说,那好,我来调用下。
1
2
3
|
int i = 3; float j = 3.0f; Console.WriteLine(IsEqual(i, j)); |
他故意传入了一个整数和一个浮点数,反正编译器不管,都是object,可以编译。但是一运行,就出错了。
甲同学说,不能这么玩的,传入的类型必须是两个相同的类型。
说着,加上一个判断:
1
2
3
4
5
6
7
|
static bool IsEqual( object a, object b) { if (a.GetType() == b.GetType()) return (a as IComparable).CompareTo(b) == 0; else return false ; } |
连类型都不一样,那就肯定不等,这下你挑不出毛病了吧。
乙同学不依不挠:
1
2
3
|
object i = new object (); object j = new object (); Console.WriteLine(IsEqual(i, j)); |
类型相同,都是object,看你怎么比。
果不其然,程序又挂掉了。
甲同学调试了下,发现object在运行时没办法转换为IComparable,所以才导致了错误,于是他还得修改代码,判断下传入的类型是否实现了IComparable……
现在我们采访下甲同学:
甲同学说,“为了让程序尽可能通用,我似乎应该使用抽象的类型,比如object,这样让调用者尽可能传各种类型过来都可以调用我的代码。
“但是,为了让程序可靠,我又不得不学着编译器那样,在运行的时候对传入的类型做前置的审查,以免调用者传入不合理的类型的对象搞破坏。如果我不想检查,还是用具体的类型(比如int、float)比较好,那样编译器就代替我的检查,不合理的参数在编译阶段就拦截下来,我可以省多少事。”
似乎这两点需求是矛盾的,那么鱼和熊掌可以兼得么?那就要使用泛型。我们看下,使用泛型我们可以怎么写:
1
2
3
4
|
static bool IsEqual<T>(T a, T b) where T : IComparable { return a.CompareTo(b) == 0; } |
我们定义了一个泛型参数T。a和b的参数类型都用T表示,这意味着编译器将会检查,a和b的类型必须相同。你想一个传int一个传float那编译器就会拦截下来了。Where后面的叫泛型约束,它保证T类型必须实现IComparable,这样我们也不用担心a和b在转换的过程中因为没有实现这一接口而在运行时出错了。所以我们也无需再写前置审查的条件了,因为编译器为我们审查了。
我们可以归纳下泛型在其中起的作用:
(1)参数使用同一个泛型参数表示它们的类型相同——甭管它们是什么类型,但是它们必须是一个类型,比如
1
2
3
4
|
void foo<T1, T2>(T1 a, T2 b, T2 c) { … } |
那就是说,b和c必须是一个类型,a是另一个类型。a可以和b、c的类型相同么?当然可以。只要T1和T2声明成相同的类型即可。a和bc类型可以相同可以不同,但是bc类型必须相同。
(2)我们可以约束某个泛型参数的基类或者实现了什么接口,比如
1
2
3
4
|
T foo<T>() where T : A { ... } |
这里,T必须是A的派生类,当然也包括了A
这样,我们无需转换,就可以直接调用A中的字段或者方法。
我们还可以增加构造函数约束,这对于我们需要在函数中直接创建T类型的对象很有用:
1
2
3
4
|
T foo<T>() where T : A, new () { return new T(); } |
在这里,因为我们约束了T可以包含一个无参数的构造函数,所以我们可以直接在代码中用new T()创建一个T的实例。如果T代表的类型没有这样的构造函数,编译器同样会在编译的时候检查出来,比如A这样定义:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
class A { private A() { } } class Program { static void Main( string [] args) { foo<A>(); } static T foo<T>() where T : A, new () { return new T(); } } |
因为A的构造函数被封闭,所以这段代码会给出一个编译错误。
除了函数可以使用泛型,类和委托也可以。
1
2
3
4
5
6
7
8
|
class A<T> { private T value; public void foo(T a) { value = a; } } |
我们给一个类加上泛型参数,那么我们可以在定义字段、属性和方法的时候用到它。此时foo方法可以直接使用T作为a的类型。而不需要在方法名后面加上<T>来定义这一参数了。
关于委托使用泛型,这里简单说下3个预置的类型:Func<>、Action<>和Predicate<T>。
它们可以使得你不必定义大部分的委托。比如你想定义一个包含2个参数,一个返回值的委托:int MyDelegate(int a, int b),你可以直接使用Func<int, int, int>。Func<T1, T2, … Tn>委托泛型的第1~n-1个参数分别表示委托参数的类型,而第n个参数表示返回值的类型。Func<T>表示没有参数,只有返回值,且返回值为T类型的委托。Action则表示方法,Action<T1, T2,…, Tn>表示具有T1~Tn,n个参数,且没有返回值的方法。void MyDelegate(int a, int b)可以表示为Action<int, int>。Action表示既没有参数,也没有返回值的方法。Predicate<>可以视作Func<>的特例,它表示返回值为bool类型的委托,因此Predicate<T1, T2, …, Tn>相当于Func<T1, T2, … Tn, bool>。注意,这三个预置的委托的n不可以无限大,在.NET 4.0中,最多有16个参数。不过我们很少有机会定义多于16个参数的函数或者方法,因此绝大多数情况下,它们够用了。
最后看个例子以结束本章:
1
2
3
4
5
6
7
8
9
10
|
class Program { static void Main( string [] args) { Func< int , int , int > add = new Func< int , int , int >(Add); Console.WriteLine(add(1, 2)); } static int Add( int a, int b) { return a + b; } } |