您的位置:首页 > 其它

协变与逆变详细解读

2017-03-09 11:44 381 查看
协变与逆变是.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也会有相应的提示,这样做也可以增加【函数传入参数值[b]】、【函数返回值】的扩展性,何乐而不为呢~[/b]
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: