协变与逆变是.Net4.0新加入的概念,我看了很多博客文章,可能是我悟性比较差,感觉没有完全讲明白,自己研究了一天终于搞懂了,特此记录一下。
一、简单理解协变和逆变
//协变:子类对象(引用)赋值给父类变量(引用) object obj = null; string str = ""; obj = str; //逆变:父类对象(引用)赋值给子类变量(引用) object obj = null; string str = ""; str = obj; /* str = obj;这段代码大家会发现是错误的,这个赋值是基本操作都是错误的, 那又如何实现逆变这种逆反的赋值操作呢?实际上逆变根本不是逆向将父类 赋给子类的,我慢慢解释... */
二、真正的协变和逆变
概念:
1、以前的泛型系统(或者说没有in/out关键字时),是不能“变”的,无论是“逆”还是“顺(协)”。
2、当前仅支持接口和委托的逆变与协变 ,不支持类和方法。但数组也有协变性。
3、值类型不参与逆变与协变。
协变:Foo<ParentClass> = Foo<ChildClass>
public class TestOut<T> where T : new() { /* * 关键字out因为协变的类型T只能作为输出参数使用,而不能作为输入参数 */ //*****[协变]泛型委托Demo***** //1、创建泛型委托 public delegate T MyFunA<T>(); //默认不支持协变与逆变 public delegate T MyFunB<out T>(); //设置支持协变 //public delegate void MyFunC<out T>(T param);//错误,协变类型只能“出”不能“入” //2、创建委托变量 public MyFunA<object> FunAObject = null; public MyFunA<string> FunAString = null; public MyFunB<object> FunBObject = null; public MyFunB<string> FunBString = null; public MyFunB<int> FunBInt = null; //3、验证结果 public void TestFun() { //FunAObject = FunAString;//错误,不可用协变 FunBObject = FunBString;//正确,可用协变可以完成子类string到父类object的转换 //FunBObject = FunBInt; //错误,值类型不参与协变 } //*****[协变]泛型接口Demo***** //1、创建泛型接口 public interface IMyInterfaceA<T> { } //默认不支持协变与逆变 public interface IMyInterfaceB<out T> { } //设置支持协变 //public interface IMyInterfaceC<out T> //{ // void Test(T param);//错误,协变只能“出”不能“入” //} //2、创建接口变量 public IMyInterfaceA<object> interAObject = null; public IMyInterfaceA<string> interAString = null; public IMyInterfaceB<object> interBObject = null; public IMyInterfaceB<string> interBString = null; public IMyInterfaceB<int> interBInt = null; //3、验证结果 public void TestInterface() { //interAObject = interAString; //错误,不可用协变 interBObject = interBString; //正确,可用协变可以完成子类string到父类object的转换 //interBObject = interBInt; //错误,值类型不参与协变 } }
逆变:Foo<ChildClass> = Foo<ParentClass>
public class TestIn<T> where T : new() { /* * 关键字in因为逆变的类型只能作为输入参数,而不能作为输出参数 */ //*****[逆变]泛型委托***** //1、创建泛型委托 public delegate void MyActionA<T>(T param); //默认不支持协变与逆变 public delegate void MyActionB<in T>(T param); //设置支持逆变 //public delegate T MyActionC<in T>();//错误,逆变只能“入”不能“出” //2、创建委托变量 public MyActionA<object> ActionAObject = null; public MyActionA<string> ActionAString = null; public MyActionB<object> ActionBObject = null; public MyActionB<string> ActionBString = null; public MyActionB<int> ActionBInt = null; //3、验证结果 public void TestAction() { //ActionAString = ActionAObject; //错误,不可用逆变 ActionBString = ActionBObject; //正确,可用逆变可以完成从父类object到子类string的转换 //ActionBInt = ActionBObject; //错误,值类型不参与逆变 } //*****[逆变]泛型接口***** //1、创建泛型接口 public interface IMyInterfaceA<T> { } //默认不支持协变与逆变 public interface IMyInterfaceB<in T> { } //设置支持逆变 //public interface IMyInterfaceC<in T> //{ // T Test();//错误,逆变只能“入”不能“出” //} //2、创建接口变量 public IMyInterfaceA<object> interAObject = null; public IMyInterfaceA<string> interAString = null; public IMyInterfaceB<object> interBObject = null; public IMyInterfaceB<string> interBString = null; public IMyInterfaceB<int> interBInt = null; //3、验证结果 public void TestInterface() { //interAString = interAObject; //错误,不可用逆变 interBString = interBObject; //正确,可用你变可以完成从父类object到子类string的转换 //interBInt = interBObject; //错误,值类型不参与逆变 } }
三、解析协变与逆变(协变是顺序的,逆变并不是逆反的)
这里就是我看别人的博客没有看懂的地方,研究时从这里卡住了半天,想通后发现豁然开朗,现在分享出来
先创建一个协变接口,一个逆变接口
//*****协变接口***** public interface ITestA<out T> { T Test(); } public class TestA<T> : ITestA<T> { public T Test() { //do something... return default(T); } } //*****逆变接口***** public interface ITestB<in T> { void Test(T p); } public class TestB<T> : ITestB<T> { public void Test(T p) { //do something... } }
协变解析:
internal class Program { private static void Main(string[] args) { //写法一 ITestA<object> testA = new TestA<string>(); object obj = testA.Test(); //写法二 ITestA<object> testA1 = null; ITestA<string> testA2 = null; testA1 = testA2; obj = testA1.Test(); /* 执行步骤如下: //先调用父类函数 public object ITestA<object>.Test() { //发现父类函数为接口,函数体由子类实现,所以... //再调用子类函数 public string ITestA<string>.Test() { //do something... } //父类函数调用子类函数,子类函数向外return返回值,由string类型传至object类型 } */ //协变“可出不可入”因为由子类函数向父类函数返回值,子类类型小,父类类型大,所以可以进行安全转换 } }
别的博客中看到以上解释,没看明白,后来才懂,他的意思是:协变时是子类向父类返回值,值类型是由子到父,可以安全转换!
[原式就是主观应该调用的方式,我想调用子类的这个函数]
[变式就是实际运行时调用的方式,先调用父类函数再由父类函数调用子类函数]
//ITest1<object> = ITest1<string> //原式(子类):string ITest1<string>.Test() // ↓ //变式(父类):object ITest1<object>.Test()
逆变解析:
internal class Program { private static void Main(string[] args) { //写法一 ITestB<string> testB = new TestB<object>(); testB.Test(""); //写法二 ITestB<string> testB1 = null; ITestB<object> testB2 = null; testB1 = testB2; testB1.Test(""); /* 执行步骤如下: //先调用父类函数 public void ITestB<string>.Test(string param) { //发现父类函数为接口,函数体由子类实现,所以... //再调用子类函数 public void ITestB<object>.Test(object param) { //do something... } //父类函数调用子类函数,并向子类函数传递参数由string类型传至object类型 } */ //逆变“可入不可出”因为由父类函数向子类函数传递参数,父类类型小,子类类型大,所以可以进行安全转换 } }
别的博客中看到以上解释,没看明白,后来才懂,他的意思是:逆变时是父类向子类传参数值,值类型是由子到父,可以安全转换!
[原式就是主观应该调用的方式,我想调用子类的这个函数]
[变式就是实际运行时调用的方式,先调用父类函数再由父类函数调用子类函数]
//ITest2<string> = ITest2<object> //原式(子类):void ITest2<object>.Test(object param) // ↑ //变式(父类):void ITest2<string>.Test(string param)
调用执行步骤:
ITestA<object> testA = new TestA<string>();
object obj = testA.Test();
ITestB<string> testB = new TestB<object>();
testB.Test("");
父类变量(引用)调用方法,实际上执行步骤如下:
1、调用父类自己的方法
2、被告知方法体由子类实现
3、父类去调用子类方法
4、【逆变】发现子类方法有参,于是父类传递自己的参数(类型string)到子类(类型object),可以安全转换
5、子类执行方法体功能
6、【协变】将执行的返回值返回给父类
7、【协变】父类接收子类方法返回值,返回值类型为子类的
8、【协变】继续向上返回,发现返回值类型不一样(类型string),所以转为父类方法的类型返回(类型object),可以安全转换
所以这就是为什么【协变只能返回值】,而【逆变只能传递值】,实际协变逆变并没有父类型转子类型的过程,都是使用的子类型转父类型的安全转换
应用场景:微软提倡只要是泛型的接口或者委托都希望使用协变逆变,RedSharper也会有相应的提示,这样做也可以增加【函数传入参数值】、【函数返回值】的扩展性,何乐而不为呢~