zoukankan      html  css  js  c++  java
  • 泛型

    第三章 泛型

    和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; }
    }
  • 相关阅读:
    234. Palindrome Linked List(判断链表是否回文)
    141. Linked List Cycle(判断链表是否有环)
    第二届“中国高校计算机大赛-大数据挑战赛” 20名
    Spark集群 Python Package管理
    Android中单选框RadioButton的基本用法
    【Android】进程间通信IPC——Binder
    Spring Boot 集成 JWT 实现单点登录授权
    pythonGUI编程——Qt库(1)
    Android获取SD卡路径/内存的几种方法
    Android主题更换换肤
  • 原文地址:https://www.cnblogs.com/sukhoi/p/7419107.html
Copyright © 2011-2022 走看看