【转】[C# 基础知识系列]专题八:深入理解泛型(二)

时间:2022-09-21 22:56:31

还是老样子,原文是转载的,不过加入了自己的思考和总结吧。

引言:

  本专题主要是承接上一个专题要继续介绍泛型的其他内容,这里就不多说了,就直接进入本专题的内容的。

 

一、类型推断

  在我们写泛型代码的时候经常有大量的"<"和">"符号,这样有时候代码一多,也难免会让开发者在阅读代码过程中会觉得有点晕的,此时我们觉得晕的时候肯定就会这样想:是不是能够省掉一些"<" 和">"符号的呢?你有这种需求了, 当然微软这位好人肯定也会帮你解决问题的,这样就有了我们这部分的内容——类型推断(其实我觉得好像也就是语法糖吧,这个观点还有待确认!),意味着编译器会在调用一个泛型方法时自动判断要使用的类型,(这里要注意的是:类型推断只使用于泛型方法,不适用于泛型类型),下面是演示代码:

 

using System;
namespace 类型推断例子
{
    class Program
    {
        static void Main(string[] args)
        {

            int n1 = 1;
            int n2 = 2;
            // 没有类型推断时需要写的代码
            // GenericMethodTest<int>(ref n1, ref n2);

            // 有了类型推断后需要写的代码
            // 此时编译器可以根据传递的实参 1和2来判断应该使用Int类型实参来调用泛型方法
            // 可以看出有了类型推断之后少了<>,这样代码多的时候可以增强可读性

            GenericMethodTest(ref n1, ref n2);
            Console.WriteLine("n1的值现在为:" + n1);
            Console.WriteLine("n2的值现在为:" + n2);
            Console.Read();

            //string t1 = "123";
            //object t2 = "456";
            //// 此时编译出错,不能推断类型
            //// 使用类型推断时,C#使用变量的数据类型,而不是使用变量引用对象的数据类型
            ////变量是一个object型,虽然它引用的是一个string,感觉好奇怪。
            //// 所以下面的代码会出错,因为C#编译器发现t1是string,而t2是一个object类型
            //// 即使 t2引用的是一个string,此时由于t1和t2是不同数据类型,???编译器所以无法推断出类型,所以报错。
            //GenericMethodTest(ref t1, ref t2);

        }
        // 类型推断的Demo
        private static void GenericMethodTest<T>(ref T t1,ref T t2)
        {
            T temp = t1;
            t1 = t2;
            t2 = temp;
        }
    }
}

代码中都有详细的注释,这里就不解释了。

二、类型约束

  如果大家看了我的上一个专题的话,就应该会注意到我在实现泛型类的时候用到了where T : IComparable,在上一个专题并没有和大家介绍这个是泛型的什么用法,这个用法就是这个部分要讲的类型约束,其实where T : IComparable这句代码也很好理解的,猜猜也明白的(如果是我不知道的话,应该是猜类型参数T要满足IComparable这个接口条件,因为Where就代表符合什么条件的意思,然而真真意思也确实如此的)下面就让我们具体看看泛型中的类型参数有哪几种约束的。首先,编译泛型代码时,C#编译器肯定会对代码进行分析,如果我们像下面定义一个泛型类型方法时,编译器就会报错:

        // 比较两个数的大小,返回大的那个
        private static T max<T>(T obj1, T obj2)
        {
            if (obj1.CompareTo(obj2) > 0)
            {
                return obj1;
            }
            return obj2;
        }    

 

  如果像上面一样定义泛型方法时,C#编译器会提示错误信息:“T”不包含“CompareTo”的定义,并且找不到可接受类型为“T”的第一个参数的扩展方法“CompareTo”。 这是因为此时类型参数T可以为任意类型,然而许多类型都没有提供CompareTo方法,所以C#编译器不能编译上面的代码,这时候我们(编译器也是这么想的)肯定会想——如果C#编译器知道类型参数T有CompareTo方法的话,这样上面的代码就可以被C#编译器验证的时候通过,就不会出现编译错误的(C#编译器感觉很人性化的,都会按照人的思考方式去解决问题的,那是因为编译器也是人开发出来的,当然会人性化的,因为开发人员当时就是这么想的,所以就把逻辑写到编译器的实现中去了),这样就让我们想对类型参数作出一定约束,缩小类型参数所代表的类型数量——这就是我们类型约束的目的,从而也很自然的有了类型参数约束这里通过对遇到的分析然后去想办法的解决的方式来引出类型约束的概念,主要是让大家可以明白C#中的语言特性提出来都是有原因,并不是说微软想提出来就提出来的,主要还是因为用户会有这样的需求,这样的方式我觉得可以让大家更加的明白C#语言特性的发展历程,从而更加深入理解C#,从我前面的专题也看的出来我这样介绍问题的方式的,不过这样也是我个人的理解,希望这样引入问题的方式对大家会有帮助,让大家更好的理解C#语言特性,如果大家对于对于有任何意见和建议的话,都可以在留言中提出的,如果觉得好的话,也麻烦表示认可下)。所以上面的代码可以指定一个类型约束,让C#编译器知道这个类型参数一定会有CompareTo方法的,这样编译器就不会报错了,我们可以将上面代码改为(代码中T:IComparable<T>为类型参数T指定的类型实参都必须实现泛型IComparable接口):

        // 比较两个数的大小,返回大的那个
        private static T max<T>(T obj1, T obj2)where T:IComparable<T>
        {
            if (obj1.CompareTo(obj2) > 0)
            {
                return obj1;
            }
            return obj2;
        }

  类型约束就是用where 关键字来限制能指定类型实参的类型数量,如上面的where T:IComparable<T>语句C# 中有4种约束可以使用,然而这4种约束的语法都差不多。(约束要放在泛型方法或泛型类型声明的末尾,并且要使用Where关键字)

(1) 引用类型约束

  表示形式为 T:class, 确保传递的类型实参必须是引用类型(注意约束的类型参数和类型本身没有关系,意思就是说定义一个泛型结构体时,泛型类型一样可以约束为引用类型,此时结构体类型本身是值类型,而类型参数约束为引用类型),可以为任何的类、接口、委托或数组等;但是注意不能指定下面特殊的引用类型:System.Object,  System.Array,  System.Delegate,  System.MulticastDelegate,  System.ValueType,  System.EnumSystem.Void.

如下面定义的泛型类:

using System.IO;  
public class samplereference<T> where T : Stream
{
     public void Test(T stream)
     {
         stream.Close();
     }
}

 

  上面代码中类型参数T设置了引用类型约束,Where T:stream的意思就是告诉编译器,传入的类型实参必须是System.IO.Stream或者从Stream中派生的一个类型,如果一个类型参数没有指定约束,则默认T为System.Object类型(相当于一个默认约束一样,就像每个类如果没有指定构造函数就会有默认的无参数构造函数,如果指定了带参数的构造函数,编译器就不会生成一个默认的构造函数)。然而,如果我们在代码中显示指定System.Object约束时,此时会编译器会报错:约束不能是特殊类“object”(这里大家可以自己试试看的)

(2)值类型约束

  表示形式为T:struct,确保传递的类型实参是值类型,其中包括枚举,但是可空类型排除,(可空类型将会在后面专题有所介绍),如下面的示例:

 // 值类型约束
 public class samplevaluetype<T> where T : struct
 {
         public static T Test()
         {
                 return new T();
         }
}    

 

  在上面代码中,new T()是可以通过编译的,因为T 是一个值类型,而所有值类型都有一个公共的无参构造函数,然而,如果T不约束,或约束为引用类型时,此时上面的代码就会报错,因为有的引用类型没有公共的无参构造函数的。

(3)构造函数类型约束

  表示形式为T:new(),如果类型参数有多个约束时,此约束必须为最后指定。确保指定的类型实参有一个公共无参构造函数的非抽象类型,这适用于:所有值类型;所有非静态、非抽象、没有显示声明的构造函数的类(前面括号中已经说了,如果显示声明带参数的构造函数,则编译器就不会为类生成一个默认的无参构造函数,大家可以通过IL反汇编程序查看下的,这里就不贴图了);显示声明了一个公共无参构造函数的所有非抽象类。(注意: 如果同时指定构造器约束和struct约束,C#编译器会认为这是一个错误,因为这样的指定是多余的,所有值类型都隐式提供一个无参公共构造函数,就如定义接口指定访问类型为public一样,编译器也会报错,因为接口一定是public的,这样的做只多余的,所以会报错。)

(4)转换类型约束

  表示形式为T:基类名(确保指定的类型实参必须是基类或派生自基类的子类)T:接口名(确保指定的类型实参必须是接口或实现了该接口的类)T:U T 提供的类型参数必须是为 U 提供的参数或派生自为 U 提供的参数。转换约束的例子如下:

(1) 引用类型约束有什么不同吗?没看出来哦。

声明

已构造类型的例子

Class Sample<T> where T: Stream

Sample<Stream>有效的

Sample<string>无效的

Class Sample<T> where T:  IDisposable

Sample<Stream >有效的

Sample<StringBuilder>无效的

Class Sample<T,U> where T: U

Sample<Stream,IDispsable>有效的

Sample<string,IDisposable>无效的

(5)组合约束(第五种约束就是前面的4种约束的组合)

  将多个不同种类的约束合并在一起的情况就是组合约束了。(注意,没有任何类型即时引用类型又是值类型的,所以引用约束和值约束不能同时使用)如果存在多个转换类型约束时,如果其中一个是类,则类必须放在接口的前面。不同的类型参数可以有不同的约束,但是他们分别要由一个单独的where关键字。下面看一些有效和无效的例子来让大家加深印象:

有效:

class Sample<T> where T:class, IDisposable, new();

class Sample<T,U> where T:class where U: struct

无效的:

class Sample<T> where T: class, struct (没有任何类型即时引用类型又是值类型的,所以为无效的)

class Sample<T> where T: Stream, class (引用类型约束应该为第一个约束,放在最前面,所以为无效的)

class Sample<T> where T: new(), Stream (构造函数约束必须放在最后面,所以为无效)

class Sample<T> where T: IDisposable, Stream(类必须放在接口前面,所以为无效的)

class Sample<T,U> where T: struct where U:class, T (类型形参“T”具有“struct”约束,因此“T”不能用作“U”的约束,所以为无效的)

class Sample<T,U> where T:Stream, U:IDisposable(不同的类型参数可以有不同的约束,但是他们分别要由一个单独的where关键字,所以为无效的)

 

三、利用反射调用泛型方法

  下面就直接通过一个例子来演示如何利用反射来动态调用泛型方法的(关于反射的内容可以我博客中的这篇文章: http://www.cnblogs.com/zhili/archive/2012/07/08/AssemblyLoad_and_Reflection.html),演示代码如下:

using System;
using System.Reflection;

namespace ReflectionGenericMethod
{
    class Program
    {
        static void Main(string[] args)
        {

            Test test = new Test();
            Type type = test.GetType();

            // 首先,获得方法的定义
            // 如果不传入BindFlags实参,GetMethod方法只返回公共成员
            // 这里我指定了NonPublic,也就是返回私有成员
            // (这里要注意的是,如果指定了Public或NonPublic的话,
            // 必须要同时指定Instance|Static,否则不返回成员,具体大家可以用代码来测试的)
            MethodInfo methodefine = type.GetMethod("PrintTypeParameterMethod", BindingFlags.NonPublic|BindingFlags.Instance|BindingFlags.Static);

            MethodInfo constructed;
            // 使用MakeGenericMethod方法来获得一个已构造的泛型方法
            constructed = methodefine.MakeGenericMethod(typeof(string));

            // 泛型方法的调用
            constructed.Invoke(null,null);
            Console.Read();
        }
    }
    public class Test
    {
        private  static void PrintTypeParameterMethod<T>()
        {
            Console.WriteLine(typeof(T));
        }
    }
}

  上面代码在调用泛型方法时传入的两个实参都是null,传入第一个为null是因为调用的是一个静态方法, 第二null是因为调用的方法是个无参的方法。 运行结果截图(结果是输出出 类型实参的类型,结果和我们预期的一样):

 【转】[C# 基础知识系列]专题八:深入理解泛型(二)

四、小结

  说到这里泛型的内容都已经介绍完了,本系列用了三个专题来介绍泛型,文章内容都基本采用提出疑问(为什么有泛型)到解释疑问,再到深入理解泛型的方式(个人认为这样的讲解方式不错的,如果大家有更好的讲解方式可以在下面留言给我),希望这种方式可以让大家知道泛型的起源,从而更好的理解泛型。后面一专题将和大家介绍了C#4.0中对泛型的改进——泛型的可变性