C#高级语法之泛型、泛型约束,类型安全、逆变和协变(思想原理)
一、为什么使用泛型?
泛型其实就是一个不确定的类型,可以用在类和方法上,泛型在声明期间没有明确的定义类型,编译完成之后会生成一个占位符,只有在调用者调用时,传入指定的类型,才会用确切的类型将占位符替换掉。
首先我们要明白,泛型是泛型,集合是集合,泛型集合就是带泛型的集合。下面我们来模仿这List集合看一下下面这个例子:
我们的目的是要写一个可以存放任何动物的集合,首先抽象出一个动物类:
//动物类
public class Animal
{
//随便定义出一个属性和方法
public String SkinColor { get; set; }//皮肤颜色
//会跑的方法
public virtual void CanRun()
{
Console.WriteLine("Animal Run Can");
}
}
然后创建Dog类和Pig类
//动物子类 Dog
public class Dog : Animal
{
//重写父类方法
public override void CanRun()
{
Console.WriteLine("Dog Can Run");
}
} //动物子类 Pig
public class Pig : Animal
{
//重写父类方法
public override void CanRun()
{
Console.WriteLine("Pig Can Run");
}
}
因为我们的目的是存放所有的动物,然后我们来写一个AnimalHouse用来存放所有动物:
//存放所有动物
public class AnimalHouse
{
//由于自己写线性表需要考虑很多东西,而且我们是要讲泛型的,所以内部就用List来实现
private List<Animal> animal = new List<Animal>(); //添加方法
public void AddAnimal(Animal a)
{
animal.Add(a);
}
//移除方法,并返回是否成功
public bool RemoveAnimal(Animal a)
{
return animal.Remove(a);
} }
AnimalHouse类型可以存放所有的动物,存放起来很方便。但是每次取出的话,使用起来会很不方便,因为只能用一些动物的特征,而无法使用子类的特征,例如Dog子类有CanSwim()方法(会游泳的方法),而动物中是没有这个方法的,所以就无法进行调用,必须将Animal类型转换为Dog类型才可以使用,不仅会增加额外的开销而且还有很大的不确定性,可能转换失败,因为AnimalHouse中是存放了很多种动物子类。
如果我们有方法可以做到,让调用者来决定添加什么类型(具体的类型,例如Dog、Pig),然后我们创建什么类型,是不是这些问题就不存在了?泛型就可以做到。
我们看一下泛型是如何定义的:
//用在类中
public class ClassName<CName>
{
//用在方法中
public void Mothed<MName>() { } //泛型类中具体使用CName
//返回值为CName并且接受一个类型为CName类型的对象
public CName GetC(CName c) {
//default关键字的作用就是返回类型的默认值
return default(CName);
}
}
其中CName和MName是可变的类型(名字也是可变的),用法的话就和类型用法一样,用的时候就把它当成具体的类型来用。
了解过泛型,接下来我们使用泛型把AnimalHouse类更改一下,将所有类型Animal更改为泛型,如下:
public class AnimalHouse<T>
{
private List<T> animal = new List<T>(); public void AddAnimal(T a)
{
animal.Add(a);
}
public bool RemoveAnimal(T a)
{
return animal.Remove(a);
} }
AnimalHouse类型想要存储什么样的动物,就可以完全交由调用者来决定:
//声明存放所有Dog类型的集合
AnimalHouse<Dog> dog = new AnimalHouse<Dog>();
//声明存放所有Pig类型的集合
AnimalHouse<Pig> pig = new AnimalHouse<Pig>();
调用方法的时候,原本写的是T类型,当声明的时候传入具体的类型之后,类中所有的T都会变成具体的类型,例如Dog类型,Pig类型
这样我们的问题就解决了,当调用者传入什么类型,我们就构造什么类型的集合来存放动物。
但是还有一个问题,就是调用者也可以不传入动物,调用者可以传入一个桌子(Desk类)、电脑(Computer),但是这些都不是我们想要的。比如我们需要调用动物的CanRun方法,让动物跑一下再放入集合里(z),因为我们知道动物都是继承自Animal类,所有动物都会有CanRun方法,但是如果传入过来一个飞Desk类我们还能使用CanRun方法吗?答案是未知的,所以为了确保安全,我们需要对传入的类型进行约束。
二、泛型约束
泛型约束就是对泛型(传入的类型)进行约束,约束就是指定该类型必须满足某些特定的特征,例如:可以被实例化、比如实现Animal类等等
我们来看一下官方文档上都有那些泛型约束:
约束 | 说明 |
where T : struct | 类型参数必须是值类型。 可以指定除 Nullable<T> 以外的任何值类型。 有关可以为 null 的类型的详细信息,请参阅可以为 null 的类型。 |
where T : class | 类型参数必须是引用类型。 此约束还应用于任何类、接口、委托或数组类型。 |
where T : unmanaged | 类型参数必须是非托管类型。 |
where T : new() | 类型参数必须具有公共无参数构造函数。 与其他约束一起使用时,new() 约束必须最后指定。 |
where T : <基类名> |
类型参数必须是指定的基类或派生自指定的基类。 |
where T : <接口名称> |
类型参数必须是指定的接口或实现指定的接口。 可指定多个接口约束。 约束接口也可以是泛型。 |
where T : U | 为 T 提供的类型参数必须是为 U 提供的参数或派生自为 U 提供的参数。 |
对多个参数应用约束:
//微软官方例子
class Base { }
class Test<T, U>
where U : struct
where T : Base, new()
{ }
使用的话只需要在泛型后面添加 where 泛型 : 泛型约束1、泛型约束2....,如果有new()约束的话则必须放在最后,说明都有很详细的介绍。
然后我们来为AnimalHouse添加泛型约束为:必须包含公共无参构造函数和基类必须是Animal
//Animal约束T必须是Animal的子类或者本身,new()约束放在最后
public class AnimalHouse<T> where T : Animal, new()
{
private List<T> animal = new List<T>(); public void AddAnimal(T a)
{
//调用CanRun方法
//如果不加Animal泛型约束是无法调用.CanRun方法的,因为类型是不确定的
a.CanRun();
//添加
animal.Add(a);
}
public bool RemoveAnimal(T a)
{
return animal.Remove(a);
} }
然后调用的时候我们传入Object试一下
提示Object类型不能传入AnimalHouse<T>中,因为无法转换为Animal类型。
我们在写一个继承Animal类的Tiger子类,然后私有化构造函数
//动物子类 Tiger
public class Tiger : Animal
{
//私有化构造函数
private Tiger()
{ }
public override void CanRun()
{
Console.WriteLine("Tiger Can Run");
}
}
然后创建AnimalHouse类型对象,传入Tiger类试一下:
提示必须是公共无参的非抽象类型构造函数。现在我们的AnimalHouse类就很完善了,可以存入所有的动物,而且只能存入动物
三、逆变和协变
先来看一个问题
Dog dog = new Dog();
Animal animal = dog;
这样写编译是不会报错的,因为Dog继承了Animal,默认会进行一个隐式转换,但是下面这样写
AnimalHouse<Dog> dogHouse = new AnimalHouse<Dog>();
AnimalHouse<Animal> animalHouse = dogHouse;
这样写的话会报一个无法转换类型的错误。
强转的话,会转换失败,我们设个断点在后一句,然后监视一下animalHouse的值,可以看到值为null
//强转编译会通过,强转的话会转换失败,值为null
IAnimalHouse<Animal> animalHouse = dogHouse as IAnimalHouse<Animal>;
协变就是为了解决这一问题的,这样做其实也是为了解决类型安全问题(百度百科):例如类型安全代码不能从其他对象的私有字段读取值。它只从定义完善的允许方式访问类型才能读取。
因为协变只能用在接口或者委托类型中,所以我们将AnimalHouse抽象抽来一个空接口IAnimalHouse,然后实现该接口:
//动物房子接口(所有动物的房子必须继承该接口,例如红砖动物房子,别墅动物房)
public interface IAnimalHouse<T> where T : Animal,new()
{ }
//实现IAnimalHouse接口
public class AnimalHouse<T> : IAnimalHouse<T> where T : Animal,new()
{
private List<T> animal = new List<T>(); public void AddAnimal(T a)
{
a.CanRun();
animal.Add(a);
}
public bool RemoveAnimal(T a)
{
return animal.Remove(a);
}
}
协变是在T泛型前使用out关键字,其他不需要做修改
public interface IAnimalHouse<out T> where T : Animal,new()
{ }
接下来我们用接口来调用一下,现在一切ok了,编译也可以通过
IAnimalHouse<Dog> dogHouse = new AnimalHouse<Dog>();
IAnimalHouse<Animal> animalHouse = dogHouse;
协变的作用就是可以将子类泛型隐式转换为父类泛型,而逆变就是将父类泛型隐式转换为子类泛型
将接口类型改为使用in关键字
public interface IAnimalHouse<in T> where T : Animal,new()
{ }
逆变就完成了:
IAnimalHouse<Animal> animalHouse = new AnimalHouse<Animal>();
IAnimalHouse<Dog> dogHouse = animalHouse;
逆变和协变还有两点:协变时泛型无法作为参数、逆变时泛型无法作为返回值。
逆变:
协变:
语法都是一些 非常粗糙的东西,重要的是思想、思想、思想。然后我们来看一下为什么要有逆变和协变?
什么叫做类型安全?C#中的类型安全个人理解大致就是:一个对象向父类转换时,会隐式安全的转换,而两种不确定可以成功转换的类型(父类转子类),转换时必须显式转换。解决了类型安全大致就是,这两种类型一定可以转换成功。(如果有错误,欢迎指正)。
协变的话我相信应该很好理解,将子类转换为父类,兼容性好,解决了类型安全(因为子类转父类是肯定可以转换成功的);而协变作为返回值是百分百的类型安全
“逆变为什么又是解决了类型安全呢?父类转子类也安全吗?不是有可能存在失败吗?”
其实逆变的内部也是实现子类转换为父类,所以说也是安全的。
“可是我明明看到的是IAnimalHouse<Dog> dogHouse = animalHouse;将父类对象赋值给了子类,你还想骗人?”
这样写确实是将父类转换为子类,不过逆变是用在作为参数传递的。这是因为写代码的“视角”原因,为什么协变这么好理解,因为子类转换父类很明显可一看出来“IAnimalHouse<Animal> animalHouse = dogHouse;”,然后我们换个“视角”,将逆变作为参数传递一下,看这个例子:
先将IAnimalHouse接口修改一下:
public interface IAnimalHouse<in T> where T : Animal,new()
{
//添加方法
void AddAnimal(T a);
//移除方法
bool RemoveAnimal(T a);
}
然后我们在主类(Main函数所在的类)中添加一个TestIn方法来说明为什么逆变是安全的:
//需要一个IAnimalHouse<Dog>类型的参数
public void TestIn(IAnimalHouse<Dog> dog) { }
接下来我们将“视角”切到TestIn中,作为第一视角,我们正在写这个方法,至于其他人如何调用我们都是不得而知的
我们就随便在当前方法中添加一个操作:为dog变量添加一个Dog对象,TestIn方法改为如下:
//需要一个IAnimalHouse<Dog>类型的参数
public static void TestIn(IAnimalHouse<Dog> dog) {
Dog d = new Dog();
dog.AddAnimal(d);
}
我们将“视角”调用者视角,如果我们想调用当前方法,只有两种方法:
//第一种
AnimalHouse<Dog> dogHouse = new AnimalHouse<Dog>();
TestIn(dogHouse);
//第二种
AnimalHouse<Animal> animalHouse = new AnimalHouse<Animal>();
//因为使用了in关键字所以可以传入父类对象
TestIn(animalHouse);
第一种的话我们就不看了,很正常也很合理,我们主要来看第二种,那第二种类型安全又在哪儿呢?
可能有人已经反应过来了,我们再来看一下TestIn方法,有一个需要传递过来的IAnimalHouse<Dog>类型的dog对象,如果调用者是使用第二种方法调用的,那这个所谓的IAnimalHouse<Dog>类型的dog对象是不是其实就是AnimalHouse<Animal>类型的对象?而dog.AddAnimal(参数类型);的参数类型是不是就是需要一个Animal类型的对象?那传入一个Dog类型的d对象是不是最终也是转换为Animal类型放入dog对象中?所以当逆变作为参数传递时,类型是安全的。
思考:那么,现在你能明白上面那个错误,为什么“协变时泛型无法作为参数、逆变时泛型无法作为返回值”了吗?
public interface IAnimalHouse<in T> where T : Animal,new()
{
//如果这样写逆变成立的话
//我们实现该接口,实现In方法,return(返回)一个默认值default(T)或者new T()
//此时使用第二种方法调用TestIn,并在TestIn中调用In方法
//注意,在TestIn中In方法的显示返回值肯定是Dog,但是实际上要返回的类型是Animal
//所以就存在Animal类型转换为Dog类型,所以就有可能失败
//所以逆变时泛型无法作为返回值
T In(); void AddAnimal(T a);
bool RemoveAnimal(T a);
}
逆变思考答案,建议自己认真思考过后再看
//在主类(Main类)中添加一个out协变测试方法
public static IAnimalHouse<Animal> TestOut() {
//返回一个子类
return new AnimalHouse<Dog>();
} //回到接口
public interface IAnimalHouse<out T> where T : Animal,new()
{
//如果这样写协变成立的话
//我们在Main方法中调用TestOut()方法,使用house变量接收一下
//IAnimalHouse<Animal> house = TestOut();
//然后调用house的AddAnimal()方法
//注意,此时AddAnimal方法需要的是一个Animal,但是实际类型却是Dog类型
//因为我们的TestOut方法返回的是一个Dog类型的对象
//所以当我们在AddAnimal()中传入new Animal()时,会存在Animal父类到Dog子类的转换
//类型是不安全的,所以协变时泛型无法作为参数
void AddAnimal(T a);
bool RemoveAnimal(T a);
}
协变思考答案,建议自己认真思考过后再看
如果我哪点讲的有误或者那点不是太明白都可以留言指正或提问。
C#高级语法之泛型、泛型约束,类型安全、逆变和协变(思想原理)的更多相关文章
- .NET Core CSharp初级篇 1-8泛型、逆变与协变
.NET Core CSharp初级篇 1-8 本节内容为泛型 为什么需要泛型 泛型是一个非常有趣的东西,他的出现对于减少代码复用率有了很大的帮助.比如说遇到两个模块的功能非常相似,只是一个是处理in ...
- .NET 4.0中的泛型逆变和协变
转载自:http://www.cnblogs.com/Ninputer/archive/2008/11/22/generic_covariant.html:自己加了一些理解 随Visual Studi ...
- Scala 深入浅出实战经典 第81讲:Scala中List的构造是的类型约束逆变、协变、下界详解
王家林亲授<DT大数据梦工厂>大数据实战视频 Scala 深入浅出实战经典(1-97讲)完整视频.PPT.代码下载:百度云盘:http://pan.baidu.com/s/1c0noOt6 ...
- ios开发ios9新特性关键字学习:泛型,逆变,协变,__kindof
一:如何去学习?都去学习什么? 1:学习优秀项目的设计思想,多问几个为什么,为什么要这么设计,这么设计的好处是什么,还能不能在优化 ,如何应用到自己的项目中 2:学习优秀项目的代码风格,代码的封装设计 ...
- 第81讲:Scala中List的构造和类型约束逆变、协变、下界详解
今天来学习一下scala中List的构造和类型约束等内容. 让我们来看一下代码 package scala.learn /** * @author zhang */abstract class Big ...
- Scala 基础(十六):泛型、类型约束-上界(Upper Bounds)/下界(lower bounds)、视图界定(View bounds)、上下文界定(Context bounds)、协变、逆变和不变
1 泛型 1)如果我们要求函数的参数可以接受任意类型.可以使用泛型,这个类型可以代表任意的数据类型. 2)例如 List,在创建 List 时,可以传入整型.字符串.浮点数等等任意类型.那是因为 Li ...
- 解读经典《C#高级编程》最全泛型协变逆变解读 页127-131.章4
前言 本篇继续讲解泛型.上一篇讲解了泛型类的定义细节.本篇继续讲解泛型接口. 泛型接口 使用泛型可定义接口,即在接口中定义的方法可以带泛型参数.然后由继承接口的类实现泛型方法.用法和继承泛型类基本没有 ...
- C#高级编程之泛型三(协变与逆变)
为何引入协变.逆变 我们知道一个子类对象可以赋值给一个基类对象 Animal animal = new Animal(); Animal cat = new Cat(); 那如果是用在泛型里面能行嘛? ...
- 在net中json序列化与反序列化 面向对象六大原则 (第一篇) 一步一步带你了解linq to Object 10分钟浅谈泛型协变与逆变
在net中json序列化与反序列化 准备好饮料,我们一起来玩玩JSON,什么是Json:一种数据表示形式,JSON:JavaScript Object Notation对象表示法 Json语法规则 ...
随机推荐
- 从0系统学Android--1.2 手把手带你搭建开发环境
要想进行程序开发,首先我们需要搭建开发环境,下面就开始搭建环境. 1.2.1 所需的工具 首先 Android 开发是基于 Java 的,因此你需要掌握简单的 Java 语法.会基础的 Java 语法 ...
- 哥们,B/S了解吗?——啥玩意,我是敲代码的
了解B/S和C/S 前言:......“学好长时间编程了,JavaSE学完了,前端也简单学了”.....“那你学这么多,讲讲B/S吧”......“B/S?这是个啥玩意?没听过”......“靠,牛逼 ...
- C#编程.面向对象编程.可删除对象(Using{})
Using关键字可以在代码块中初始化使用重要资源的对象,Dispose()方法会在这个代码块的末尾自动调用,用法如下: <ClassName> <VariableName> = ...
- 小白学python-day01-电脑结构知识
作为一个”0“基础的经济学学士来说,专业分类选择了经统,多多少少和计算机有点关系,从今天开始学习python. 但行努力,莫问前程. day01学习电脑结构等知识. 因为这些知识是 有规则的,客观的文 ...
- mysql协议分析2---认证包
主人看到navicat和mysql在那嘻嘻哈哈,眉来眼去的,好不快乐,忽然也想自己写个程序,直接去访问Mysql,虽然现在已经有很多现成的中间件可以直接拿来用了,程序只要负责写sql语句就行了,但是主 ...
- 温故而知新,重温 Java 7 的那些“新”特性
2009 年 4 月 20 日,Java 的亲生父亲 Sun 被养父 Oracle 以 74 亿美元收购,这在当时可是一件天大的事.有不少同学都担心 Java 的前途,我当时傻不啦叽地也很担心:自己刚 ...
- +CIMG+彩色图片边缘提取实验记录_canny/hough transfrom
前言: 书到用时方恨少 正文: 边缘提取技术一直都有接触,最通用的莫过于拉普拉斯,sobel几个算子,两个算子都可通过简单的模板运算进行,而现在比较好的一个边缘提取技术是canny,文章中我是用的ca ...
- 多线程编程(Linux C)
多线程编程可以说每个程序员的基本功,同时也是开发中的难点之一,本文以Linux C为例,讲述了线程的创建及常用的几种线程同步的方式,最后对多线程编程进行了总结与思考并给出代码示例. 一.创建线程 多线 ...
- postman使用pre-request script计算md5
接口加了验签逻辑,具体是md5(salt+时间戳).被某君吐槽说测试不方便啊能不能先关掉.其实没有必要打开又关闭验签功能,postman的pre-request script功能完全可以模拟客户端加密 ...
- 9-1、大型项目的接口自动化实践记录----数据库结果、JSON对比
上一篇写了如何从DB获取预期.实际结果,这一篇分别对不同情况说下怎么进行对比. PS:这部分在JSON对比中也适用. 1.结果只有一张表,只有一条数据 数据格式:因为返回的是dicts_list的格式 ...