C#精粹--协变和逆变

概念

协变和逆变来源于类型和类型之间的绑定,C#4.0开始在泛型的接口和委托上支持协变和逆变,不过在这个版本之前的委托也是支持协变和逆变的。比如数组就支持协变,但是这不是一个好的特性,这C#初期版本从java引入的一个特性,当时的设计者认为C#应该尽可能的像java的一些特性靠拢,因为java当时太火了。

如果有类型Parent和其子类Sub,那么Parent p=new Sub();这种的类型转换是安全的。如果有一种类型和Parent类型进行了绑定,比如说Parent[]数组,如果Sub[]到Parent[]的转换是安全的,我们就说是协变,如果相反的方向上转换是安全的,我们就说是逆变了。

C#的协变和逆变没有完整的阐述这个数学和物理上的概念,C#为协变和逆变的实现捆绑了很多条件:

①必须是泛型的委托或接口

②协变只能支持返回类型

③逆变只能支持参数

而这些条件总归是实现了一个面向对象原则:里氏替换原则。也就是说,不管是协变还是逆变,最终都是一种类型安全的转换,没有不安全的转换发生。

有了上面这些条件的约束还不够,C#还明确的指定了两个关键字来让开发人员更加明确的知道声明的泛型类型参数到底用在了哪里:给协变对应的关键字是out,逆变对应的关键字是in,看一下示例:

IEnumerable<out T>//IEnumerable这个接口对T协变
Action<int TParameter>//Action这个委托对TParameter逆变

上面这个解释基本阐明了泛型的协变和逆变的概念,下面来看一些例子

示例

委托的协变和逆变

    public delegate object Test(string obj);
    class Program
    {

        static void Main(string[] args)
        {
            Test test = TestMethod;
            var result = test("hello world");
            Console.ReadKey();

            string TestMethod(object str)
            {
                Console.WriteLine(str);
                return "hello world";
            }
        }
    }

代码第一行定义了一个返回object,输入string的名为Test的委托。我们在main方法中定义了一个本地方法(C#7的功能)然后用这个方法来实例化一个Test委托(利用方法组转换特性),可以看到这个Test委托既表达了协变的意思(返回类型的协变:object=string,由一个object类型引用一个返回string类型的方法肯定没有问题),又表达了逆变的意思(参数类型的逆变:实际上还是object=string,为什么这么说呢?因为真正要执行的方法是TestMethod,这个方法需要一个object类型的参数,但是这个方法的地址指向了一个Test委托,这个委托代表了那种输入类型为string,输出类型为object的方法,当我们调用Test(...)这个委托时,很明显需要传入一个string类型的变量进去,但是真正执行的方法是TestMethod,而这个TestMethod需要传入一个object类型的参数,那么,这个string类型传入这个TestMethod方法内部肯定是没有问题的,同时,也说明了,逆变实际上还是一种安全转换,按这个例子来说(参数要求是object的,实际传入了一个string),还是从string类型转换成了object类型了嘛!)。

上面解释有点儿啰嗦,但也基本说明了原理了。

协变

接口的协变最著名的例子就是IEnumerable<T>:

IEnumerable<object> ss = new List<string>();

逆变

逆变的例子是一个Action<T>:

public static void Main(string[] args)
        { 
            Action<string> action = Test;
            void Test(object obj)
            {
                Console.WriteLine(obj);
            }
        }

这些关于协变和逆变的解释在上面通通都有,就不再解释了。

协变和逆变的相互作用

这是一个相当有趣的话题,我们先来看一个例子:

interface IFoo<in T>
{ 
}
interface IBar<in T>
{
  void Test(IFoo<T> foo); //对吗?
}

你能看出上述代码有什么问题吗?我声明了in T,然后将他用于方法的参数了,一切正常。但出乎你意料的是,这段代码是无法编译通过的!反而是这样的代码通过了编译:

 

interface IFoo<out T>
{ 
}
interface IBar<in T>
{
  void Test(IFoo<T> foo); //对吗?
}

 

什么?明明是out参数,我们却要将其用于方法的参数才合法?初看起来的确会有一些惊奇。我们需要费一些周折来理解这个问题。现在我们考虑IBar<string>,它应该能够协变成IBar<object>,因为string是object的子类。因此IBar.Test(IFoo<string>)也就协变成了IBar.Test(IFoo<object>)。当我们调用这个协变后方法时,将会传入一个IFoo<object>作为参数。想一想,这个方法是从IBar.Test(IFoo<string>)协变来的,所以参数IFoo<object>必须能够变成IFoo<string>才能满足原函数的需要。这里对IFoo<object>的要求是它能够反变成IFoo<string>!而不是协变。也就是说,如果一个接口需要对T协变,那么这个接口所有方法的参数类型必须支持对T的反变。同理我们也可以看出,如果接口要支持对T反变,那么接口中方法的参数类型都必须支持对T协变才行。这就是方法参数的协变-反变互换原则。所以,我们并不能简单地说out参数只能用于返回值,它确实只能直接用于声明返回值类型,但是只要一个支持反变的类型协助,out类型参数就也可以用于参数类型!换句话说,in参数除了直接声明方法参数之外,也仅能借助支持协变的类型才能用于方法参数,仅支持对T反变的类型作为方法参数也是不允许的。要想深刻理解这一概念,第一次看可能会有点绕,建议有条件的情况下多进行一些实验。

刚才提到了方法参数上协变和反变的相互影响。那么方法的返回值会不会有同样的问题呢?我们看如下代码:

interface IFooCo<out T>
{

}
interface IFooContra<in T>
{

}
interface IBar<out T1, in T2>
{
    IFooCo<T1> Test1();

    IFooContra<T2> Test2();
}

我们看到和刚刚正好相反,如果一个接口需要对T进行协变或反变,那么这个接口所有方法的返回值类型必须支持对T同样方向的协变或反变这就是方法返回值的协变-反变一致原则。也就是说,即使in参数也可以用于方法的返回值类型,只要借助一个可以反变的类型作为桥梁即可。如果对这个过程还不是特别清楚,建议也是写一些代码来进行实验。至此我们发现协变和反变有许多有趣的特性,以至于在代码里in和out都不像他们字面意思那么好理解。当你看到in参数出现在返回值类型,out参数出现在参数类型时,千万别晕倒,用本文的知识即可破解其中奥妙。

文章部分引用自:https://www.cnblogs.com/Ninputer/archive/2008/11/22/generic_covariant.html

 

原文地址:https://www.cnblogs.com/pangjianxin/p/8620138.html