一、怎么理解协变逆变
协变和逆变维基上写的很复杂,但是总结起来原理其实就一个。
1.子类型可以隐性的转换为父类型
说个最容易理解的例子,int和float两个类型的关系可以写成下面这样。
int ≦ float :也就是说int是float的子类型。
按照上面的原理来说,就是int可以转换成float类型,比如int 3可以默认转化为float 3.0,但是float 3.14默认转换成int 3 感觉总是怪怪的。所以我们说3可以是float类型,但是3.14不能是int类型。
2.现在开始说协变和逆变。
维基百科的解释是下面酱紫的。
- 协变(covariant),如果它保持了子类型序关系≦。该序关系是:子类型≦基类型。
- 逆变(contravariant),如果它逆转了子类型序关系。
绝大部分的语言是允许协变的,也就是上面说的子类型可以默认转换为父类型,逆变一般是不被允许的(除了函数的参数)。
所以。。。函数的subtyping往往很难理解对不对(嗯,一定是酱紫的)
我们继续沿用刚才的例子好了,比如有一个函数foo( x ),他的类型是float->int(接受一个float类型的参数,返回int类型的值,具体构造可以忽略掉)。从subtying的关系来看,float->int ≦ int->float(这里参数是逆变的),所以按照原理来说,可以说foo( x )拥有int->float类型,但是为什么是这样的呢。
其实是酱紫的,当我们把foo函数当作int->float类型调用的时候,编译器会自动的在函数上加上“前缀”和“后缀”:先把参数从int类型转换成float类型,然后再传递给函数(这个函数只能接受float类型),再将函数的返回值int类型(这个函数只能输出int类型)转换成float类型作为输出。函数本身是没有变化的。
这两个转换都是从子类型int转换成父类型float所以没有任何问题,那么float->int类型转换成int->float类型也就没有任何问题了。反过来看如果是将int->float转换成float->int类型将面临两次float到int类型的转换,这明显是不被允许的。
就酱。
二、使用函数的不同阶段发生的类型转换
假设有一函数,接收 object 类型的参数,输出 string 类型的返回值:
string Method(object o)
{
return "abc";
}
那么在Main函数中我们可以这样调用它:
string s = "abc";
object o = Method(s);
注意,这里发生了两次隐式类型转换:
- 在向函数输入时,参数 s 由 string 类型转换为 object 类型
- 在函数输出(返回)时,返回值 由 string 类型转换为 object 类型
我们这里可以看作是函数签名可发生变换(不论函数的内容,不影响结果):
- string Method(object o) 可变换为 string Method(string o)
- string Method(string o) 可变换为 object Method(string o)
也就是说,在函数输入时,函数的 输入类型 可由 object 变换为 string,父->子
在函数输出时,函数的 输出类型 可由string变换为object,子->父
三、理解泛型接口中的 in、out参数
1.没有指定in、out的情况
假设有一泛型接口,并且有一个类实现了此接口:
interface IDemo<T>
{
T Method(T value);
}
public class Demo : IDemo<string>
{
//实现接口 IDemo<string>
public string Method(string value)
{
return value;
}
}
在Main函数中这样写:
IDemo<string> demoStr = new Demo();
IDemo<object> demoObj = demoStr;//报错
上面的这段代码中的第二行包含了一个假设:
IDemo<string> 类型能够隐式转换为 IDemo<object> 类型
这乍看上去就像“子类型引用转换为父类型引用” 一样,然而很遗憾,他们并不相同。假如可以进行隐式类型转换,那就意味着:
string Method(string value) 能转换为 object Method(object value)
从上一节中我们知道,在函数这输入和输出阶段,其类型可变化方向是不同的。
所以在C#中,要想应用泛型接口类型的隐式转换,需要讨论“输入”和“输出”两种情况。
2.接口仅用于输出的情况,协变
interface IDemo<out T>
{
//仅将类型 T 用于输出
T Method(object value);
}
public class Demo : IDemo<string>
{
//实现接口
public string Method (object value)
{
//别忘了类型转换!
return value.ToString();
}
}
在Main函数中这样写:
IDemo<string> demoStr = new Demo();
IDemo<object> demoObj = demoStr;
可将 string Method (object value) 转换为 object Method (object value)
即可将 IDemo<string> 类型转换为 IDemo<object> 类型。
仅从泛型的类型上看,这是 “子->父” 的转换,与第一节中提到的转换方向相同,称之为“协变”。
3.接口仅用于输入的情况,逆变
同理我们可以给 T 加上 in 参数:
interface IDemo<in T>
{
//仅将类型 T 用于输入
string Method(T value);
}
public class Demo : IDemo<object>
{
//实现接口
public string Method (object value)
{
return value.ToString();
}
}
在Main函数中这样写:
IDemo<object> demoObj = new Demo();
IDemo<string> demoStr = demoObj;
这里可将 string Method (object value) 转换为 string Method (string value)
即可将 IDemo<object> 类型转换为 IDemo<string> 类型。
仅从泛型的类型上看,这是 “父->子” 的转换,与第一节中提到的转换方向相反,称之为“逆变”,有时也译作“抗变”或“反变”。










网友评论