C#中的协变与逆变

Posted Hello Bug.

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C#中的协变与逆变相关的知识,希望对你有一定的参考价值。

一:什么是协变与逆变

官方解释:
协变指能够使用比原始指定的派生类型的派生程度更大(更具体的)的类型,逆变指能够使用比原始指定的派生类型的派生程度更小(不太具体的)的类型 
更易理解的:
如果某个返回的类型可以由其派生类型替换,那么这个类型就是支持协变的
如果某个参数类型可以由其基类替换,那么这个类型就是支持逆变的

只有泛型接口和泛型委托参数支持协变和逆变


二:引言

using System;
using System.Collections.Generic;

class MainClass
{
    static void Main()
    {
        object o = "str";
        List<object> oList = new List<string>();
        IEnumerable<object> strs = new List<string>();
    }
}

上面这段代码中,第一句没问题,属于类型安全转换,第二句会报错,因为这两个list并没有继承关系,而第三句是正确的,其实在背后,就是协变和逆变在起作用


三:协变

协变在泛型方法的参数里以out表示,使用out可以在声明父类泛型参数的时候使用子类泛型参数构造,out参数只能用在输出位置,作为返回值
例如IEnumerable<T>接口

using System;
using System.Collections.Generic;

class MainClass
{
    static void Main()
    {
        IEnumerable<object> list = new List<string>();
    }
}

分析一下上面的代码为什么是合法的呢?首先虽然是用IEnumerable<object>声明的,但是是用List<string>构造的,列表中的元素是string类型,输出的时候是string转换成了object,本质上还是派生类到基类的转换,所以是类型安全的,本质上就是里氏替换原则


四:逆变

逆变在泛型方法的参数里以in表示,使用in可以在声明子类泛型参数的时候使用父类泛型参数构造,int参数只能用在输入位置,作为传入值
例如Action<T>委托

using System;

class MainClass
{
    static void Main()
    {
        Action<string> action = new Action<object>((o)=> { });
        action("");
    }
}

分析一下上面的代码为什么是合法的呢?看似是object转换成了string,但实际上使用委托的时候传入的是一个string类型的参数,然后将string转换成了object,本质上还是派生类到基类的转换,所以是类型安全的,本质上就是里氏替换原则


五:为什么协变和逆变是针对泛型接口或泛型委托参数的?而不能针对泛型类?

涉及到类型转换,必定要考虑类型安全的问题,由上面协变和逆变的例子分析出对于泛型接口或泛型委托的参数本质上就是派生类向基类的转换,遵循里氏替换原则,所以是类型安全的。而泛型类中是允许定义字段的,字段之间的转换就有可能存在类型不安全的情况,但是接口和委托都是定义方法的,类型必然安全

using System;
using System.Collections.Generic;

class MainClass
{
    static void Main()
    {
        object o1 = "";//类型安全
        string s1 = (string)o1;//非类型安全
        IEnumerable<object> o2 = new List<string>();//协变
        Action<string> s2 = new Action<object>((o) => { });//逆变
    }
}

六:自定义协变

using System;
using System.Collections.Generic;

class MainClass
{
    static void Main()
    {
        ICustomCovariant<object> o = new CustomCovariant<string>();
    }
}

public interface ICustomCovariant<out T>
{
    T Get();
}
public class CustomCovariant< T> : ICustomCovariant<T>
{
    public T Get()
    {
        return default(T);
    }
}

七:自定义逆变

using System;
using System.Collections.Generic;

class MainClass
{
    static void Main()
    {
        IContravariant<string> o = new CustomContravariant<object>();
    }
}

public interface IContravariant<in T>
{
    void Get(T t);
}
public class CustomContravariant<T> : IContravariant<T>
{
    public void Get(T t)
    {

    }
}

八:总结

——在泛型中,如果确定泛型参数是只读或者只写的,那么就可以使用协变或者逆变。如果泛型参数无法确定只读或只写,这种类型参数既不能协变也不能逆变,只能精确类型匹配
——在泛型或委托中,如果不使用协变或逆变,那么泛型类型是一个固定类型,而使用协变或逆变的话,则泛型类型可以实现多态化

——协变和逆变只针对于引用类型,值类型不参与协变和逆变
——声明属性时要注意,只有只读属性才允许使用out类型参数,只写属性才允许使用in类型参数

以上是关于C#中的协变与逆变的主要内容,如果未能解决你的问题,请参考以下文章

JAVA中的协变与逆变

厘清泛型参数的协变与逆变

协变与逆变

java逆变与协变(待完善)

了解 C# 中的协变和逆变接口

为啥 C# (4.0) 不允许泛型类类型中的协变和逆变?