基于.Net中的协变与逆变的深入分析
程序员文章站
2024-03-01 12:42:28
关于协变和逆变要从面向对象继承说起。继承关系是指子类和父类之间的关系;子类从父类继承所以子类的实例也就是父类的实例。比如说animal是父类,dog是从animal继承的子...
关于协变和逆变要从面向对象继承说起。继承关系是指子类和父类之间的关系;子类从父类继承所以子类的实例也就是父类的实例。比如说animal是父类,dog是从animal继承的子类;如果一个对象的类型是dog,那么他必然是animal。
协变逆变正是利用继承关系 对不同参数类型或返回值类型 的委托或者泛型接口之间做转变。我承认这句话很绕,如果你也觉得绕不妨往下看看。
如果一个方法要接受dog参数,那么另一个接受animal参数的方法肯定也可以接受这个方法的参数,这是animal向dog方向的转变是逆变。如果一个方法要求的返回值是animal,那么返回dog的方法肯定是可以满足其返回值要求的,这是dog向animal方向的转变是协变。
由子类向父类方向转变是协变 协变用于返回值类型用out关键字
由父类向子类方向转变是逆变 逆变用于方法的参数类型用in关键字
协变逆变中的协逆是相对于继承关系的继承链方向而言的。
一. 数组的协变:
复制代码 代码如下:
animal[] animalarray = new dog[]{};
上面一行代码是合法的,声明的数组数据类型是animal,而实际上赋值时给的是dog数组;每一个dog对象都可以安全的转变为animal。dog向animal方法转变是沿着继承链向上转变的所以是协变
二. 委托中的协变和逆变
1.委托中的协变
复制代码 代码如下:
//委托定义的返回值是animal类型是父类
public delegate animal getanimal();
//委托方法实现中的返回值是dog,是子类
static dog getdog(){return new dog();}
//getdog的返回值是dog, dog是animal的子类;返回一个dog肯定就相当于返回了一个animal;所以下面对委托的赋值是有效的
getanimal getmethod = getdog;
2.委托中的逆变
复制代码 代码如下:
//委托中的定义参数类型是dog
public delegate void feeddog(dog target);
//实际方法中的参数类型是animal
static void feedanimal(animal target){}
// feedanimal是feeddog委托的有效方法,因为委托接受的参数类型是dog;而feedanimal接受的参数是animal,dog是可以隐式转变成animal的,所以委托可以安全的的做类型转换,正确的执行委托方法;
feeddog feeddogmethod = feedanimal;
定义委托时的参数是子类,实际上委托方法的参数是更宽泛的父类animal,是父类向子类方向转变,是逆变
三. 泛型委托的协变和逆变:
1. 泛型委托中的逆变
如下委托声明:
复制代码 代码如下:
public delegate void feed<in t>(t target);
feed委托接受一个泛型类型t,注意在泛型的尖括号中有一个in关键字,这个关键字的作用是告诉编译器在对委托赋值时类型t可能要做逆变
复制代码 代码如下:
//先声明一个t为animal的委托
feed<animal> feedanimalmethod = a=>console.writeline(“feed animal lambda”);
//将t为animal的委托赋值给t为dog的委托变量,这是合法的,因为在定义泛型委托时有in关键字,如果把in关键字去掉,编译器会认为不合法
feed<dog> feeddogmethod = feedanimalmethod;
2. 泛型委托中的协变
如下委托声明:
复制代码 代码如下:
public delegate t find<out t>();
find委托要返回一个泛型类型t的实例,在泛型的尖括号中有一个out关键字,该关键字表明t类型是可能要做协变的
复制代码 代码如下:
//声明find<dog>委托
find<dog> finddog = ()=>new dog();
//声明find<animal>委托,并将finddog赋值给findanimal是合法的,类型t从dog向animal转变是协变
find<animal> findanimal = finddog;
四. 泛型接口中的协变和逆变:
泛型接口中的协变逆变和泛型委托中的非常类似,只是将泛型定义的尖括号部分换到了接口的定义上。
1.泛型接口中的逆变
如下接口定义:
复制代码 代码如下:
public interface ifeedable<in t>
{
void feed(t t);
}
接口的泛型t之前有一个in关键字,来表明这个泛型接口可能要做逆变
如下泛型类型feedimp<t>,实现上面的泛型接口;需要注意的是协变和逆变关键字in,out是不能在泛型类中使用的,编译器不允许
复制代码 代码如下:
public class feedimp<t>:ifeedable<t>
{
public void feed(t t){
console.writeline(“feed animal”);
}
}
来看一个使用接口逆变的例子:
复制代码 代码如下:
ifeedable<dog> feeddog = new feedimp<animal>();
上面的代码将feedimp<animal>类型赋值给了ifeedable<dog>的变量;animal向dog转变了,所以是逆变
2.泛型接口中的协变
如下接口的定义:
复制代码 代码如下:
public interface ifinder<out t>
{
t find();
}
泛型接口的泛型t之前用了out关键字来说明此接口是可能要做协变的;如下泛型接口实现类
复制代码 代码如下:
public class finder<t>:ifinder<t> where t:new()
{
public t find(){
return new t();
}
}
//使用协变,ifinder的泛型类型是animal,但是由于有out关键字,我可以将finder<dog>赋值给它
ifinder<animal> finder = new finder<dog>();
协变和逆变的概念不太容易理解,可以通过实际代码思考理解。这么绕的东西到底有用吗?答案是肯定的,通过协变和逆变可以更好的复用代码。复用是软件开发的一个永恒的追求。