.Net泛型详解

引言

在我们使用.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修饰的类型参数只能用作输入。

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

    //假设我们有两个类,Dog类是Animal的子类
    public class Animal
    {
        public virtual void Write()
        {
            Console.WriteLine("我是基类");
        }
    }

    public class Dog : Animal
    {
        public override void Write()
        {
            Console.WriteLine("我是小小狗");
        }
    }

然后我们定义一个支持协变的接口和一个类(类是用来后面创建实例的,因为接口无法直接new),用out关键字修饰类型参数:

    //支持协变的接口,用out关键字修饰
    public interface ICovariant<out T>
    {

    }
    //写一个类继承于ICovariant
    public class Covariant<T> : ICovariant<T>
    {

    }

然后我们会发现,IConveriant<Dog>的实例能够给一个IConveriant<Animal>的变量,这就称为协变。子类给父类。

            ICovariant<Dog> dogCovariant = new Covariant<Dog>();
            ICovariant<Animal> animalCovariant = dogCovariant;//协变的体现

然后根据以上规律,我们狭义的去总结协变:所谓协变就是当用out关键字修饰了接口或者委托的类型参数之后,对于该泛型,子类类型参数的实例能够赋值给一个该泛型父类类型参数的变量。用伪代码表示就是IConvariant<FatherClass> father=new IConvariant<ChildClass>();

下面我们去理解逆变,逆变是刚好相反的:
同样是上面的代码,我们把IConvariant的类型参数修饰的关键字改成in:

    //支持协变的接口,用out关键字修饰
    public interface ICovariant<in T>//修改了类型参数的关键字
    {

    }
    //写一个类继承于ICovariant
    public class Covariant<T> : ICovariant<T>
    {

    }

然后我们写下如下代码:

            ICovariant<Animal> animalCovariant = new Covariant<Animal>();
            ICovariant<Dog> dogCovariant = animalCovariant;//逆变的体现

我们会发现以上两句代码居然能够编译通过,经过in关键字修饰了类型变量的泛型。其父类类型变量的泛型实例能够给一个使用了子类类型参数的泛型变量。刚好反过来了....。这就是逆变,用伪代码表示:IConvariant<ChildClass> child=new IConvariant<FatherClass>()。

一些重点:

一:对于泛型,也许我们对于如下代码会产生疑问:

    public interface ICovariant<in T>
    {

    }
    //写一个类继承于ICovariant
    public class Covariant<T> : ICovariant<T>
    {

    }

对于Convariant<T>,到底是继承于IConvariant的所有可能的类型参数,还是只继承于相同类型参数。举个例子:
Covariant<Animal>到底只继承于IConvariant<Animal>还是既继承于IConvariant<Animal>又继承于IConvariant<Dog>。这里本人的理解是,泛型和泛型之间的继承,只有相同的类型参数才有继承关系。就是Covariant<Animal>到底只继承于IConvariant<Animal>

二:父类的泛型约束能够被子类继承吗?

答案显然是否定的,继承关系我们只听说继承属性,方法,字段,没听说能继承约束的。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

孤行者程序之路

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值