引言

在我们使用.Net进行编程的过程中经常遇到这样的场景:对于几乎相同的处理,由于入参的不同,我们需要写N多个重载,而执行过程几乎是相同的。更或者,对于几乎完成相同功能的类,由于其内部元素类型的不同,我们需要写N多个同质化的类。对于这样的场景,我们可以通过使用泛型来进行处理。

泛型的概念

泛型允许我们对类、接口、委托、方法使用一个或多个占位符来延迟声明,在实际调用的时候再补充这些占位符,以便让编译器对不同类型的占位符进行不同的类型编译,这是一种延迟声明的思想体现。

当然,以上是笔者本人的理解,我们也可以采用一些大牛的理解:可利用泛型创建一个数据结构,该数据结构能够进行特化以处理特定的类型,程序员定义这种"参数化类型",使泛型类型的每个变量具有相同内部算法,但数据类型和方法签名可随类型参数而变。

我们经常看到泛型声明使用的占位符"T",而这个占位符就称为这个泛型的"类型参数",而实际调用这个泛型的时候这个"类型参数"又称为"类型实参"。

好了,概念就到这里,反正我是觉得只看概念越看越懵。

泛型的作用

泛型的作用本人理解就是能够减少代码量,给予特定的类或者方法或者委托的等一种通用的代码,而一套通用的代码可以适用于处理各种数据类型,以避免为每种数据类型的处理声明一个类或者方法。

当然,这还是容易理解,要理解他的作用,我们先做出这样的考虑:如果没有泛型会怎样?

下面比如我们有一个这样的需求:

我们需要写一个方法,就是在控制台打印入参(当然,尽管这可能是毫无意义的),假设这个方法需要支持int,string,double三个类型。

面对这样的需求,我们有两种解决方案,第一种,就是定义一个方法三个重载,分别打印int、string、double类型的入参,代码如下:

        static void PrintParameter(int parameter)
        {
            Console.WriteLine(parameter);
        }
        static void PrintParameter(string parameter)
        {
            Console.WriteLine(parameter);
        }
        static void PrintParameter(double parameter)
        {
            Console.WriteLine(parameter);
        }

以上解决方式肯定不科学,因为这样的一个简单的处理我们竟然写了三个重载,代码量太多了。那么我们可以采取另外一种方式:将方法的入参声明为object类型,因为object类型是所有类型的基类,于是能够支持各种类型的入参,进而打印。

        static void PrintParameter(object parameter)
        {
            Console.WriteLine(parameter);
        }

这种方式虽然能够解决问题,但是有一个性能弊端,比如我们传入的类型是int,double或者结构类型这些值类型呢?那么难免程序会有拆装箱操作,而这样的操作是对性能有损失的。

那么有既能避免性能损失,又能避免代码冗余的方式吗?

泛型的作用就这样体现了!

我们可以定义一个如下的方法:

        static void PrintParamter<T>(T parameter)
        {
            Console.WriteLine(parameter);
        }

以上代码我们使用了尖括号包裹的"类型参数"T作为占位符,在实际调用的时候我们只需要指定T的类型,编译器就能够将这些占位符编译成对应的类型,并且既是类型安全的又避免了拆装箱。

所以,在泛型的众多应用中,主要作用是两个方面1:提高性能。2:避免代码冗余。当然,还有很多其他的作用,这个我们在具体编程的时候能够体会到。

泛型的分类和对应的定义语法

泛型根据其主体的不同可以分为泛型类、泛型方法、泛型接口、泛型委托四种类型,下面分别是四种类型的定义:

    //这是一个泛型类
    public class GenericClass<T>
    {
        //这是一个泛型方法
        public void GenericMethod<U,P>(U para1,P para2)
        {
            
        }
        //这是一个泛型委托
        public delegate K GenericDelegate<K>();
    }
    //这是一个泛型接口
    public interface IGenericInterface<T>
    {

    }

通过观察以上简略的定义我们可以发现如下规律:
1:类型参数不只是可以为T,也可以为其他字符或者字符串,因为只是一个占位符而已,叫什么名字不重要。

2:一个泛型的类型参数可以不止一个,可以有,两个、三个甚至更多。而一个泛型的类型参数的个数我们称之为“元数”。以上案例中的GenericMethod显然是一个二元泛型方法。

3:我们为类指定了类型参数之后还能为类中的方法或者委托指定另外的类型参数,当然,实际这种使用的情况很少。

泛型约束

在一般的使用中,我们使用T类型参数来代表一切类型,但是在特定的场景下我们还是希望对类型参数进行进一步的限定,以进一步的保证类型安全,并获得对类型参数的更高的访问权限。

这里"更高的访问权限"可能暂时无法理解,我们举个例子:

比如我们定义了一个动物类Animal,代码如下:

    /// <summary>
    /// 定义有两个属性的动物类
    /// AnimalTypeName:动物的种类名称,如:老虎
    /// AnimalHeight:动物的身高,如:10.2
    /// </summary>
    public class Animal
    {
        public string AnimalTypeName { get; set; }
        public double AnimalHeight { get; set; }
    }

然后我们在控制台中定义了一个方法ShowName,用来显示传入对象的名字,用T作为类型参数,因为传入的有可能是动物,有可能是人:

        /// <summary>
        /// 事实上这个方法在语法上是不通过的,因为无法确定入参t,拥有AnimalTypeName属性
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="t"></param>
        static void ShowName<T>(T t)
        {
            Console.WriteLine(t.AnimalTypeName);
        }

以上ShowName方法在语法上绝对是错的,因为我们无法确保传入的类型实参一定是Animal类型的或者说是一定拥有AnimalTypeName属性。那么既然代码已经写了,我们是否可以限定类型参数的范围呢,限定T必须为Animal类型。当然可以,代码如下:

        /// <summary>
        /// 使用where T:Animal来约束类型参数T必须是Animal类型或者其子类
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="t"></param>
        static void ShowName<T>(T t) where T: Animal
        {
            Console.WriteLine(t.AnimalTypeName);
        }

我们在ShowName方法后面加入约束where T:Animal,这样就限定了类型参数必须是Animal或者其子类。这个约束,就叫做"基类约束"

那么除了基类约束还有那些约束呢?下面我们用代码予一一举例:

接口约束(约束类型实参必须继承于某接口)

        /// <summary>
        /// 接口约束,约束类型实参必须继承于某接口
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="t"></param>
        static void ShowName<T>(T t) where T : ICloneable
        {
            object obj=t.Clone();
        }

引用类型约束(约束类型实参必须是引用类型)

        /// <summary>
        /// 引用类型约束:约束类型实参必须是引用类型
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="t"></param>
        static void ShowName<T>(T t) where T : class
        {
            
        }

值类型约束(约束类型实参必须是值类型)

        /// <summary>
        /// 值类型约束:约束类型实参必须是值类型
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="t"></param>
        static void ShowName<T>(T t) where T : struct
        {

        }

无参构造函数约束(约束类型实参必须拥有无参的构造函数)

        /// <summary>
        /// 无参构造函数约束:约束类型实参必须拥有无参的构造函数
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="t"></param>
        static void ShowName<T>(T t) where T : new()
        {

        }

多重约束(约束多个方面)

        /// <summary>
        /// 多重约束:以下案例约束了类型参数必须为引用类型,且必须继承接口ICloneable,且必须有无参的构造函数
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="t"></param>
        static void ShowName<T>(T t) where T : class, ICloneable,new()
        {

        }

协变和逆变论述

看了很多资料,就很少发现把协变和逆变讲清楚了的,这里本人自己总结一下吧,如果总结的不好或者有偏差望朋友们指出。

所谓协变,本人认为就是和谐的变,这里指子类向父类的转变。任何子类实例都可以赋值给一个父类变量,这就是协变。

所谓逆变,就是不和谐的变,这里指父类向子类的转变,父类实例给一个子类变量。

泛型中,我们只可以对接口和委托使用协变和逆变。分别用关键字out和in,用out修饰的类型参数只能用作返回,用in修饰的类型参数只能用作输入。

下面是协变案例(虽然是毫无意义的代码),实现协变:

    //定义一个泛型接口和一个泛型类,类继承于接口,且接口类型参数用关键字out修饰,表示协变
    public interface IMeanlessInterface<out T>
    {

    }
    public class MeanlessClass<T> : IMeanlessInterface<T>
    {

    }
        static void Test()
        {
            //声明一个接口变量
            IMeanlessInterface<object> large;
            //将一个MeanlessClass类的实例给这个接口变量,不报错
            MeanlessClass<string> small = new MeanlessClass<string>();
            //结论:使用out关键字修饰了类型参数T的接口支持协变
            large = small;
        }

下面是逆变案例(代码毫无意义,主要是意思):

    /// <summary>
    /// 声明一个泛型委托,类型参数用in修饰
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public delegate void MeanlessDelegate<in T>();
            //声明一个委托类型的变量参数类型使用string类型,然后创建一个MeanlessDelegate委托实例,参数类型为object类型
            //然后将委托实例给委托变量,发现不报错
            //结论:用in修饰类型参数的委托支持逆变
            MeanlessDelegate<string> dele = new MeanlessDelegate<object>(()=> { });

协变和逆变都只能对引用类型的类型参数使用,不能适用于值类型的类型参数。

06-08 23:03