收藏

C# 协变 逆变

2022-01-30  本文已影响0人  合肥黑
一、怎么理解协变逆变

参考
知乎 应该怎么理解编程语言中的协变逆变?作者:一朵云

协变和逆变维基上写的很复杂,但是总结起来原理其实就一个。

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.现在开始说协变和逆变。

维基百科的解释是下面酱紫的。

绝大部分的语言是允许协变的,也就是上面说的子类型可以默认转换为父类型,逆变一般是不被允许的(除了函数的参数)。
所以。。。函数的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类型的转换,这明显是不被允许的。
就酱。

以下参考 理解 C# 泛型接口中的协变与逆变(抗变)

二、使用函数的不同阶段发生的类型转换

假设有一函数,接收 object 类型的参数,输出 string 类型的返回值:

string Method(object o)
{
    return "abc";
}

那么在Main函数中我们可以这样调用它:

string s = "abc";
object o = Method(s);

注意,这里发生了两次隐式类型转换:

我们这里可以看作是函数签名可发生变换(不论函数的内容,不影响结果):

也就是说,在函数输入时,函数的 输入类型 可由 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> 类型。
仅从泛型的类型上看,这是 “父->子” 的转换,与第一节中提到的转换方向相反,称之为“逆变”,有时也译作“抗变”或“反变”。

上一篇 下一篇

猜你喜欢

热点阅读