.NET Core CSharp初级篇 1-8

本节内容为泛型

为什么需要泛型

泛型是一个非常有趣的东西,他的出现对于减少代码复用率有了很大的帮助。比如说遇到两个模块的功能非常相似,只是一个是处理int数据,另一个是处理string数据,或者其他自定义的数据类型,但我们没有办法,只能分别写多个方法处理每个数据类型,因为方法的参数类型不同。有没有一种办法,在方法中传入通用的数据类型,这样不就可以合并代码了吗?

泛型简介

在我们的C#中,使用泛型对允许您延迟编写类或方法中的编程元素的数据类型的规范,直到实际在程序中使用它的时候。换句话说,泛型允许您编写一个可以与任何数据类型一起工作的类或方法。泛型的定义非常简单,在类或函数名后使用作为占位符即可,这个T也可以换成其他的字母代替。

注意:属性和索引器不能指定自己的泛型参数,它们只能使用所属类中定义的泛型参数进行操作。

你可以通过下面这个例子得到一些关于泛型定义的方法。

值得注意的是泛型是在运行时进行动态变化,并不是在编译时发生。

泛型类与泛型函数

泛型类和泛型函数在使用上基本上是一样的,只不过定义后的范围不一样。对于泛型类,泛型的范围是整个类,泛型函数则是在函数内部。

例如这个例子

class A<T>
{
public T getSomething<X>(X m,T n)
{
return n;
}
public static U test<U>(U x)
{
return x;
}
}
// 实例泛型类必须指定类型
A<string> a = new A<string>()
//泛型推断
A.test<int>(1);//原式
A.test(1);//推断

在泛型函数的调用中,有一个语法糖,它就是泛型类型推断。这非常好理解,C#的编译器足够聪明,它可以根据你传入的参数类型,调用gettype方法进行类型的推断。因此你可以在泛型函数中不显式的指定类型。

类型推理的相同规则适用于静态方法和实例方法。 编译器可基于传入的方法参数推断类型参数;而无法仅根据约束或返回值推断类型参数。 因此,类型推理不适用于不具有参数的方法。 类型推理发生在编译时,之后编译器尝试解析重载的方法签名。 编译器将类型推理逻辑应用于共用同一名称的所有泛型方法。 在重载解决方案步骤中,编译器仅包含在其上类型推理成功的泛型方法。

泛型的范围则是包含关系。包含在泛型类中的泛型函数可以自由的访问泛型类中的泛型,但是类不可以访问泛型函数中指定的泛型。

泛型约束

如果我们使用了泛型,那么必定面临的一个问题就是权限问题。例如class A,假定我希望某些类型不可以作为泛型传入,那么我们就应当使用我们的泛型约束。

泛型约束的使用如下例:

class A : T where T:class
{ }

泛型约束通常有下面几类:

  • where T : struct:类型参数必须是值类型。可以指定除 Nullable 以外的任何值类型。
  • where T : class 类型参数必须是引用类型。 此约束还应用于任何类、接口、委托或数组类型。
  • where T : unmanaged 类型参数不能是引用类型,并且任何嵌套级别均不能包含任何引用类型成员。
  • where T : new() 类型参数必须具有公共无参数构造函数。 与其他约束一起使用时,new() 约束必须最后指定。
  • where T : <基类名> 类型参数必须是指定的基类或派生自指定的基类。
  • where T : <接口名称> 类型参数必须是指定的接口或实现指定的接口。 可指定多个接口约束。 约束接口也可以是泛型。
  • where T : U 为 T 提供的类型参数必须是为 U 提供的参数或派生自为 U 提供的参数。

某些约束是互斥的。 所有值类型必须具有可访问的无参数构造函数。 struct 约束包含 new() 约束,且 new() 约束不能与 struct 约束结合使用。 unmanaged 约束包含 struct 约束。 unmanaged 约束不能与 struct 或 new() 约束结合使用。使用的时候稍加注意即可。

你也可以指定多个类型占位符,并且单独为他们进行约束,如:

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

甚至你可以进行泛型自我约束,例如:

class A<T,U,K>
where T:struct
where U:K

协变和逆变

这三个名词来自于数学和物理,很多初学者都难以理解这些名词。但事实上在C#上,这些词是用于标示类型与类型之间的绑定。可变性是以一种类型安全的方式,将一个对象当做另一个对象来使用。如果不能将一个类型替换为另一个类型,那么这个类型就称之为:不变。

协变

如果某个返回的类型可以由其派生类型替换,那么这个类型就是支持协变的。直白的说,协变就是合理的变化,例如猫->动物,这个看上去丝毫没有问题。这就是协变,从小变大。

例如:

// Cat:Animal
//这种变化毫无问题
Cat c = new Cat();
Animal a = c;
//报错,因为List<Cat>不继承于List<Animal>
List<Cat> d = new List<Cat>();
List<Animal> = d;

对于泛型的参数,我们可以使用到我们之前讲函数参数的时候所遇到的 in,out 关键字。In代表输入,体现的就是逆变,Out代表输出,代表的是协变。对于Out输出的东西,自然不可以对他进行输入操作,他只能作为结果返回,因此它不会被修改。因此进行隐式转换的时候,编译器认为该转换是安全的(返回值不变)。

例如

    IEnumerable<Cat> c = new List<Cat>();

    IEnumerable<Animal> a = c;

很多人可能不不能很好地理解这些来自于物理和数学的名词。我们无需去了解他们的数学定义,但是至少应该能分清协变和逆变。实际上这个词来源于类型和类型之间的绑定。我们从数组开始理解。数组其实就是一种和具体类型之间发生绑定的类型。数组类型Int32[]就对应于Int32这个原本的类型。任何类型T都有其对应的数组类型T[]。那么我们的问题就来了,如果两个类型T和U之间存在一种安全的隐式转换,那么对应的数组类型T[]和U[]之间是否也存在这种转换呢?这就牵扯到了将原本类型上存在的类型转换映射到他们的数组类型上的能力,这种能力就称为“可变性(Variance)”。在.NET世界中,唯一允许可变性的类型转换就是由继承关系带来的“子类引用->父类引用”转换。举个例子,就是String类型继承自Object类型,所以任何String的引用都可以安全地转换为Object引用。我们发现String[]数组类型的引用也继承了这种转换能力,它可以转换成Object[]数组类型的引用,数组这种与原始类型转换方向相同的可变性就称作协变

逆变

逆变则恰恰与协变完全相反,逆变是指代类型往更小的派生类中进行转换,显然这有可能是不安全的,因为有可能会导致数据的丢失。在C#中使用逆变式的方法是使用In关键字,这意味着这个参数只能作为返回值返回,那么我们就有可能对传入的参数进行修改,因此使用强制转换有可能是不合法的。

例如:

public interface IMyList<in T>
{
T GetElement();
void ChangeT(T t);
} public class MyList<T> : IMyList<T>
{
public T GetElement()
{
return default(T);
}
public void ChangeT(T t)
{
//Change T
}
}

这段代码无法通过编译,因为GetElement是将T返回,这显然不符合逆变的定义。逆变的参数只允许输入而不允许输出。

对于逆变的实践,各位可以去参阅下IList接口与IEnumerable接口的实现。这两个接口很好的体现了在集合中的逆变与协变。

(缺少举例说明,后续补上)

总结

对于泛型,并没有太多的奇技淫巧可言,因为泛型的出现已经就是一个奇技淫巧了。泛型最常用的地方是泛型数组。并且C#对于不确定类型和大小的数组会使用一个非常好用的类,叫做List类,我们将会在中级篇中进行详细的介绍。

如果我的文章帮助了您,请您在github .NET Core Guide项目帮我点一个star,在博客园中点一个关注和推荐。

Github

BiliBili主页

WarrenRyan's Blog

博客园

.NET Core CSharp初级篇 1-8泛型、逆变与协变的更多相关文章

  1. .NET Core CSharp初级篇 1-1

    .NET Core CSharp初级篇 1-1 本节内容是对于C#基础类型的存储方式以及C#基础类型的理论介绍 基础数据类型介绍 例如以下这句话:"张三是一名程序员,今年15岁重50.3kg ...

  2. NET Core CSharp初级篇 1-3面向对象

    .NET Core CSharp初级篇 1-3 本节内容为面向对象初级教程 类 简介 面向对象是整个C#中最核心最有特色的一个模块了,它很好的诠释了程序与现实世界的联系. 面向对象的三大特征:继承.多 ...

  3. .NET Core CSharp初级篇 1-5 接口、枚举、抽象

    .NET Core CSharp初级篇 1-5 本节内容类的接口.枚举.抽象 简介 问题 如果你需要表示星期或者是某些状态,使用字符串或者数字是否不直观? 你是否发现,无论何种电脑,它的USB口的设计 ...

  4. .NET Core CSharp初级篇 1-6 类的多态与继承

    .NET Core CSharp初级篇 1-6 本节内容为类的多态与继承 简介 终于讲到了面向对象三大特性中的两大特性--继承与多态.通过继承与多态,我们能很好的将类的拓展性发挥到了极致.在下面的内容 ...

  5. .NET Core CSharp初级篇 类的生命历程

    .NET Core CSharp初级篇 1-7 本节内容为类的生命周期 引言 对象究竟是一个什么东西?对于许多初学者而言,对象都是一个非常抽象的知识点.如果非要用一句话描述,我觉得"万物皆对 ...

  6. .NET Core CSharp初级篇 1-2 循环与判断

    .NET Core CSharp初级篇 1-2 本节内容循环与判断 循环 循环是一个在任何语言都是极为重要的语法,它可以用于很多东西,例如迭代数组等等.在C#中,语法层面的循环有:for , fore ...

  7. .NET 4.0中的泛型逆变和协变

    转载自:http://www.cnblogs.com/Ninputer/archive/2008/11/22/generic_covariant.html:自己加了一些理解 随Visual Studi ...

  8. CSharp初级篇 1-4 this、索引器、静态、常量以及只读

    .NET Core CSharp初级篇 1-4 本节内容为this.索引器.静态.常量以及只读 简介 在之前的课程中,我们谈论过了静态函数和字段的一小部分知识,本节内容中,我们将详细的讲解关于对象操作 ...

  9. .NET Core CSharp 中级篇 2-2 List,ArrayList和Dictionary

    .NET Core CSharp 中级篇 2-2 本节内容为List,ArrayList,和Dictionary 简介 在此前的文章中我们学习了数组的使用,但是数组有一个很大的问题就是存储空间不足,我 ...

随机推荐

  1. 使用nodejs-koa2-mysql-sequelize-jwt 实现项目api接口

    nodejs-koa2-mysql-sequelize-jwt 技术栈:nodejs, koa2, mysql, sequelize, jwt 项目数据层和操作层分明 使用koa2框架中间件,参数处理 ...

  2. 不一样的go语言-玩转语法之二

      本文继续玩转语法,是为之二.   I/O(Input/Output),输入输出是计算机最为突出的特点,也可以说是计算机最为核心的功能.没有I/O,计算机就是一堆废铜废铁.从最低层的电子元器件开始, ...

  3. surging 微服务引擎 2.0 会有多少惊喜?

    surging 微服务引擎从2017年6月至今已经有两年的时间,这两年时间有多家公司使用surging 服务引擎,并且有公司搭建了CI/CD,并且使用了k8s 集群,这里我可以说下几家公司的服务搭建情 ...

  4. 【Flink】Flink 底层RPC框架分析

    1. 前言 对于Flink中各个组件(JobMaster.TaskManager.Dispatcher等),其底层RPC框架基于Akka实现,本文着重分析Flink中的Rpc框架实现机制及梳理其通信流 ...

  5. Spring Bean的3种装配方式

    Bean常用的装配方式有3种: 基于xml的装配 基于Annotation(注解)的装配 基于Java类的装配 基于xml的装配 在xml文件中配置Bean. 如果依赖很多,xml配置文件会很臃肿,后 ...

  6. Linux实例/etc/fstab文件配置错误导致系统启动异常

    Centos 7.3系统 问题现象: 阿里云ECS升级配置后重启,SSH连接不上.登录控制台远程连接ECS,出现以下界面.  提交工单阿里云反馈:https://help.aliyun.com/kno ...

  7. shell写的俄罗斯方块

    共享一下. #!/bin/bash # Tetris Game # xhchen<[email]xhchen@winbond.com.tw[/email]> #APP declaratio ...

  8. 2019攻防世界web新手区

    robots 看了题目描述,发现与robots协议有关,过完去百度robots协议.发现了robots.txt,然后去构造url访问这个文件 http://111.198.29.45:42287/ro ...

  9. HashMap原理(二) 扩容机制及存取原理

    我们在上一个章节<HashMap原理(一) 概念和底层架构>中讲解了HashMap的存储数据结构以及常用的概念及变量,包括capacity容量,threshold变量和loadFactor ...

  10. TreeView虚拟化跳转

    使用ItemContainerGenerator.ContainerFromItem方法可以获取对应数据的UIElement . 但是如果使用了虚拟化技术,超出可见区域的UIElement就获取不到了 ...