第三章 泛型

和Java、C++一样,区别于Python、Javascript、Ruby等语言,C#是强类型语言。所谓强类型,就是指编译器会在编译阶段检查类型和对象调用的合法性。

在第一章,我们举了一个例子:

C# code
 
?
1
2
int i = 1;
i.CompareTo(2);



这个代码是合法的,但是

C# code
 
?
1
2
object i = 1;
i.CompareTo(2);



这样的代码是非法的。因为编译器会检查i的类型,它是object的,而object类型并没有CompareTo这个方法。有人说了,为什么编译器不能“智能一点”,根据i=1推断出i就是int类型呢。

那你冤枉编译器了,有时候,这真不好推断:

C# code
 
?
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的类型究竟是整数还是字符串——我们也不知道,除非程序运行,用户输入了以后才知道。

总而言之,编译器在编译的时候必须确定一个变量的类型,因为它会把它和调用它的代码硬编码到可执行文件中去,所以它不得不确认这一点。

编译器不但会检查一个变量是否能调用某个方法,如上面所述的那样,还会检查某个对象能不能传给这个变量,比如

C# code
 
?
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类型究竟表示什么具体的类型,我们有时候还是得等运行的时候才知道。编译器不敢乱猜。

我们来编写一个函数,比较两个数是否相等:

同学甲不假思索编写了如下代码:

C# code
 
?
1
2
3
4
static bool IsEqual(int a, int b)
{
    return a == b;
}



同学乙马上反驳道,你怎么知道是整数?如果是浮点数呢?
甲同学说,这个好办,我再写一个就是了:

C# code
 
?
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;
}



但是马上意识到,这样写很呆,不过他马上就想到一个“好办法”:

C# code
 
?
1
2
3
4
static bool IsEqual(object a, object b)
{
    return (a as IComparable).CompareTo(b) == 0;
}



使用object类型代替了具体的类型,这下不管传入什么,编译器都统统放行。

乙同学说,那好,我来调用下。

C# code
 
?
1
2
3
int i = 3;
float j = 3.0f;
Console.WriteLine(IsEqual(i, j));



他故意传入了一个整数和一个浮点数,反正编译器不管,都是object,可以编译。但是一运行,就出错了。

甲同学说,不能这么玩的,传入的类型必须是两个相同的类型。

说着,加上一个判断:

C# code
 
?
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;
}



连类型都不一样,那就肯定不等,这下你挑不出毛病了吧。

乙同学不依不挠:

C# code
 
?
1
2
3
object i = new object();
object j = new object();
Console.WriteLine(IsEqual(i, j));



类型相同,都是object,看你怎么比。

果不其然,程序又挂掉了。

甲同学调试了下,发现object在运行时没办法转换为IComparable,所以才导致了错误,于是他还得修改代码,判断下传入的类型是否实现了IComparable……

现在我们采访下甲同学:

甲同学说,“为了让程序尽可能通用,我似乎应该使用抽象的类型,比如object,这样让调用者尽可能传各种类型过来都可以调用我的代码。

“但是,为了让程序可靠,我又不得不学着编译器那样,在运行的时候对传入的类型做前置的审查,以免调用者传入不合理的类型的对象搞破坏。如果我不想检查,还是用具体的类型(比如int、float)比较好,那样编译器就代替我的检查,不合理的参数在编译阶段就拦截下来,我可以省多少事。”

似乎这两点需求是矛盾的,那么鱼和熊掌可以兼得么?那就要使用泛型。我们看下,使用泛型我们可以怎么写:

C# code
 
?
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)参数使用同一个泛型参数表示它们的类型相同——甭管它们是什么类型,但是它们必须是一个类型,比如

C# code
 
?
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)我们可以约束某个泛型参数的基类或者实现了什么接口,比如

C# code
 
?
1
2
3
4
T foo<T>() where T : A
    ...
}



这里,T必须是A的派生类,当然也包括了A
这样,我们无需转换,就可以直接调用A中的字段或者方法。

我们还可以增加构造函数约束,这对于我们需要在函数中直接创建T类型的对象很有用:

C# code
 
?
1
2
3
4
T foo<T>() where T : A, new()
{
    return new T();
}



在这里,因为我们约束了T可以包含一个无参数的构造函数,所以我们可以直接在代码中用new T()创建一个T的实例。如果T代表的类型没有这样的构造函数,编译器同样会在编译的时候检查出来,比如A这样定义:

C# code
 
?
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的构造函数被封闭,所以这段代码会给出一个编译错误。

除了函数可以使用泛型,类和委托也可以。

C# code
 
?
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个参数的函数或者方法,因此绝大多数情况下,它们够用了。

最后看个例子以结束本章:

C# code
 
?
1
2
3
4
5
6
7
8
9
10
class Program
{
    static void Main(string[] args)
    {
        Func<intintint> add = new Func<intintint>(Add);
        Console.WriteLine(add(1, 2));
    }
 
    static int Add(int a, int b) { return a + b; }
}
posted on 2017-08-23 17:06  SUKHOIIII  阅读(120)  评论(0编辑  收藏  举报