C#泛型

泛型

​ 对于泛型的解释我们先看下百科的解释:泛型是程序设计语言的一种特性。允许程序员在强类型程序设计语言中编写代码时定义一些可变部分,那些部分在使用前必须作出指明。各种程序设计语言和其编译器、运行环境对泛型的支持均不一样。将类型参数化以达到代码复用提高软件开发工作效率的一种数据类型。泛型类是引用类型,是堆对象,主要是引入了类型参数这个概念。

​ 泛型是 .Net Framework 2.0提出的,我对他的理解就是 泛型的出现就是为了解决用一个方法,满足不同的参数类型,做相同的事的需求。

1、没有泛型的年代

我们只能看类型写方法,尽管他们的功能是一样的参数是不同类型的,那时候我们没有办法,例如:

​ 当然,我们承认一点就是说,ShowString() 和ShowInt()这两个方法绝对是最快的,因为它就是自己类型的操作一点多余的损耗也没有, 但是没用啊 !! 你看着这么多重复的代码你不烦吗?

我们是这样重复代码的,那时候能够解决不同参数的办法只有一个,就是用基类 Object

2、泛型的出现

泛型的出现上面说了是.Net Framework 2.0出现的,他的作用就是为了解决相同功能不同参数的需求。

先说名一下,代码中的 T 不仅仅可以是任意东西,中文应该也可以,主要不是系统关键字都可。

我们对比一下我们上面写的ShowObject方法,这两者有什么区别呢?

区别就在于 Object是所有类型的父类,它可以转换成任何类型,但是在转换的时候Objcet 会有装箱和拆箱两个过程,这样会多一倍的损耗。

我们可能疑问了,为什么Object 会有装箱拆箱 而泛型却没有?

这个就要说到了泛型的机制了,泛型的运行方法是 推迟声明,推迟一切能推迟的东西。泛型没有写死参数类型,推迟到调用的时候才去指定参数类型

3、泛型的调用

很简单吧,用一对尖括号里面指定参数类型,后面的括号里面指定实参就行,只要注意好一点就可以,尖括号里面的别和后面小括号的类型一致

当然要说的就是 没有尖括号也行,因为我们说了泛型是延迟声明,系统会 也不应该是说系统 应该是说你的编译器会帮你自动推算这个类型应该是什么。

4、泛型是如何工作的呢?

​ 控制台程序最终会编译成一个exe程序,exe被点击的时候,会经过JIT(即时编译器)的编译,最终会生成二进制代码,才能被计算机执行,泛型加入到语法以后,VS自带的编译器又做了升级,升级之后编译时遇到泛型,会做特殊的处理:生成占位符。再次经过JIT编译的时候,会把上面编译成的占位符替换成具体的数据类型。

请看下面一个例子:

Console.WriteLine(typeof(List<>));
Console.WriteLine(typeof(Dictionary<,>));

结果:

​ 从上面的截图中可以看出:泛型在编译之后会生成占位符。占位符以后如果有时间的话,可以仔细研究下。

注意:占位符需要在英文输入法状态下才能输入,只需要按一次波浪线(数字1左边的键位)的键位即可,不需要按Shift键。

5、 泛型类

​ 泛型类封装了不针对任何特定数据类型的操作。泛型类常用于容器类,如链表、哈希表、栈、队列、树等等。这些类中的操作,如对容器添加、删除元素,不论所存储的数据是何种类型,都执行几乎同样的操作。

6、泛型接口

​ 不论是为泛型容器类,还是表示容器中元素的泛型类,定义接口是很有用的。把泛型接口与泛型类结合使用是更好的用法,比如用IComparable而非IComparable,以避免值类型上的装箱和拆箱操作。

7、泛型委托

8、普通类可以继承泛型类

在C#中申明的普通类也可以继承泛型类,但是我没有遇到过这样的需求,感觉碰到的可能性不是很大。注意泛型在声明的时候可以不指定具体的类型,但是在使用的时候必须指定具体类型

如果子类也是泛型的,那么继承的时候可以不指定具体类型。

9、泛型约束

​ 要检查表中的一个元素,以确定它是否合法或是否可以与其他元素相比较,那么编译器必须保证:客户代码中可能出现的所有类型参数,都要支持所需调用的操作或方法。这种保证是通过在泛型类的定义中,应用一个或多个约束而得到的。一个约束类型是一种基类约束,它通知编译器,只有这个类型的对象或从这个类型派生的对象,可被用作类型参数。一旦编译器得到这样的保证,它就允许在泛型类中调用这个类型的方法。上下文关键字where用以实现约束。同一个类型参数可应用多个约束。约束自身也可以是泛型类。并且泛型的约束可以是多个且可以多种类型。

class MyList<T> where T: Employee, IEmployee,  IComparable<T>,  new() {…}

下面是五种约束

约束 描述
where T: struct 类型参数必须为值类型。
where T : class 类型参数必须为引用类型。
where T : new() 类型参数必须有一个公有、无参的构造函数。当于其它约束联合使用时,new()约束必须放在最后。
**where T : ** 类型参数必须是指定的基类型或是派生自指定的基类型。
**where T : ** 类型参数必须是指定的接口或是指定接口的实现。可以指定多个接口约束。接口约束也可以是泛型的。

我们一个一个来

 public   class Person
    {
        public int ID { get; set; }
        public string Name { get; set; }
        public void Hello()
        { 
        }
    }
    public class Chinese : Person, IWork, ISports
    {
        public void WeShouldDo()
        {
            Console.WriteLine("武汉加油!!湖北加油!!");
        }


        public void Walk()
        {
            Console.WriteLine("特殊时期不要出去!");
        }

        public void Work()
        {
            Console.WriteLine("去工作的路上记得带口罩!");
        }
    }

    public class WuHan:Person
    {
        public void BeHappy()
        {
            Console.WriteLine("等我们病好了,请大家来看樱花");
        }
           
    }

    public interface ISports
    {
        void Walk();
    }
    public interface IWork
    {
        void Work();
    }

代码中我们写了一个Person类 Chinese 和WuHan的实体类,并且写了两个接口,也给他们实现了一下。

下面我们来看泛型的实例化

    Console.WriteLine("***** 泛型实例化 *******");
    Person person = new Person()
    {
        ID = 1,
        Name = "person"
    };
    Chinese chinese = new Chinese()
    {
        ID = 2,
        Name = "Chinese"
    };
    WuHan wuHan = new WuHan()
    {
        ID = 3,
        Name = "WuHan"
    };
    Console.Read();

下面如果说我们有个需求是要打印参数的ID和Name ,我们可以这样写

  public static void Show<T> (T value)
  {
      Console.WriteLine($"{value.ID}--- {value.Name}");
  }

但是这样写他会报错,

​ 我们看错也知道了为什么会报错,因为我们说了,泛型是任何参数类型都能传进来,他不知道你传进来的是谁啊,所以他不可能知道你有ID和Name,所以传递的之后我们要做一下约束,约束一下 传递进来的只能是Person 类型的。

当然了,我们也可以进行多个约束,也可以进行多种约束。

在其中我们看到了 多种约束直接在后面写Where 条件预计就可以,而且我们还可以看出 泛型的参数是可以是中文的。

其他的约束我就不讲了,上面的表格中列出了的那五种约束可以看一下。

奥,对了要说一点,约束不可以约束密封类,也不可以约束值类型,因为值类型是密封的,就相当于我们不能约束 Int String这样的类型

我们反编译看到了 string类型的源码是 sealed密封类的

image-20200130114545246

而 Int 是值类型的

所以不可以约束他们

10、为什么要用泛型约束而不用基类约束呢?

既然我们知道了传进来的一定是个Person类型的,为什么不直接用一个Person 接收呢?

​ 这个的话,我只能说,看需求吧,如果你以后的需求要变,给Person加一些条件的话,用泛型约束最好,因为条件我们可以随意加啊,但是如果是确定这个功能不变了,就只是传进来一个Person而已的话,基类的会稍微好一点。

11、逆变 斜边(Out In)

我们看代码

我们声明了一个Bird类和一个Swallow 的燕子类,我们看下面四种实例化方式,没啥说的,唯一要说的就声明的时候可以用父类作为主类实例化子类,但是子类不可以作为主类实例化父类。

我们照着这个逻辑就可以推出来泛型应该也符合这个机制。

但是我们发现报错了,但是按照我们前面的逻辑应该是可以啊, 一堆燕子就是一堆鸟啊 ,为什么会报错呢,鸟不是燕子的父类吗?

这个报错的原因是 在这里面他们没有父子关系 记住偶 这是泛型,虽然里面的元素是相同的,但是泛型只认数据不认人 ,就说说这两个东西放到了两个不同的List集合里面,他们没有父子关系。

当然,我们想实现的话也可以实现,解决办法就是要把类型转换一下 这是泛型发布之后最开始时候的坑

下面进入正题,讲到了我们的 out 和 In

out 和 In是 4.0出现的

这个T必须是返回值类型,不能出现在参数里面

斜边只能用在接口或者委托之中,他的特点就是

也可以自己写

逆变:左边子类 右边可以是子类也可以是父类

协变 逆变一起来

image-20200130001755254

posted @ 2020-01-30 02:41  乾子  阅读(478)  评论(0编辑  收藏  举报