标签:span rri 私有字段 文档 子类 监视 抽象类 main方法 ima
首先我们要明白,泛型是泛型,集合是集合,泛型集合就是带泛型的集合。下面我们来模仿这List集合看一下下面这个例子:
//动物类 public class Animal { //随便定义出一个属性和方法 public String SkinColor { get; set; }//皮肤颜色 //会跑的方法 public virtual void CanRun() { Console.WriteLine("Animal Run Can"); } }
//动物子类 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类型可以存放所有的动物,但是每次存入子类对象的时候就会进行装箱操作,每次取出的话,还要再次进行拆箱操作,会消耗额外的性能,因为所有的子类都能存放,所以拆箱的话也会很麻烦。
我们看一下泛型是如何定义的:
//用在类中 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;
逆变和协变还有两点:协变时泛型无法作为参数、逆变时泛型无法作为返回值。
逆变:
协变:
先将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#高级语法之泛型、泛型约束,类型安全、逆变和协变(思想原理)
标签:span rri 私有字段 文档 子类 监视 抽象类 main方法 ima
原文地址:https://www.cnblogs.com/ckka/p/11395777.html