雁过请留痕...
代码改变世界

协变(covariant)和逆变(contravariant)

2014-01-21 16:15  xiashengwang  阅读(738)  评论(1编辑  收藏  举报

我们知道子类转换到父类,在C#中是能够隐式转换的。这种子类到父类的转换就是协变

而另外一种类似于父类转向子类的变换,可以简单的理解为“逆变”。

上面对逆变的简单理解有些牵强,因为协变和逆变只能针对接口和代理类型。而父类和子类之间不存在这种逆变的概念。

协变和逆变的本质都是子类安全的转到父类的过程。

下面就来加深下印象,先定义两个类Car和Baoma

    public class Car
    {
    }

    public class Baoma : Car
    {
    }

明显Baoma(宝马)是Car的子类

1,先来看看协变

协变在C#中要用out关键字标明,用这个关键字就表示参数T只能用于函数,属性等的返回值。

    //协变(covariant)
    public interface ICo<out T>
    {
        T Test1();
    }
            //协变(covariant)
            ICo<Car> ico1 = null;
            ICo<Baoma> ico2 = null;
            ico1 = ico2;//子类-->父类
            Car car = ico1.Test1();//实际调用ico2.Test1()返回Baoma类型,当然可以赋值给父类Car
            //ico2 = ico1;//error
            //Baoma baoma = ico2.Test1();//实际调用ico1.Test1()返回Car类型,赋值给子类Baoma,显然不能保证类型安全

2,在来看看逆变

逆变在C#中用in关键字标明,表明参数T只能用于函数的参数。

    //逆变(contravariant)
    public interface IContra<in T>
    {
        void Test1(T t);
    }
            //逆变(contravariant)
            IContra<Car> icontra1 = null;
            IContra<Baoma> icontral2 = null;
            //icontra1 = icontral2;//error
            //icontra1.Test(new Car()); 实际调用IContra<Baoma>.Test(Baoma),参数Car不能安全的转换为Baoma
            icontral2 = icontra1;//看似很奇怪,但这里不是Car->Baoma,而是IContra<Car>->IContra<Baoma>
            icontral2.Test1(new Baoma());//实际调用IContra<Car>.Test(Car),参数Baoma能安全的转换为Car

上面的代码中已经标注详细了调用的过程,对于理解很有帮助。下面分析两种复杂一点的过程。

3,带有逆变或协变的接口作为函数参数

    public interface IFoo<in T>
    {
        void Test(T t);
    }
    public interface IBar<out T>//这里T必须用out,而不是in
    {
        void Test(IFoo<T> foo);
    }
            IFoo<Car> ifoo1 = null;
            IBar<Car> ibar1 = null;
            IBar<Baoma> ibar2 = null;
            ibar1 = ibar2;//协变
            ibar1.Test(ifoo1);//实际调用IBar<Baoma>的Test(IFoo<Baoma>),
            //要求IFoo<Car>要转换到IFoo<Baoma>,这就需要IFoo对T逆变,即用in关键字

引用装配脑袋对这种情况的归纳:

//如果一个接口需要对T协变,那么这个接口所有方法的参数类型必须支持对T的反变。
//同理我们也可以看出,如果接口要支持对T反变,那么接口中方法的参数类型都必须支持对T协变才行。
//这就是方法参数的协变-反变互换原则。

4,带有逆变或协变的接口作为函数的返回值

    public interface IFoo<in T>
    {
        void Test(T t);
    }
    public interface IBar2<in T>
    {
        IFoo<T> Test();
    }
            IBar2<Car> a = null;
            IBar2<Baoma> b = null;
            b = a;//逆变
            IFoo<Baoma> ibaoma = b.Test();//实际调用IBar2<Car>.Test返回IFoo<Car>类型,IFoo<Car>要转换成IFoo<Baoma>
                                          //需要IFoo对T逆变,这种函数返回值的转换方向是一致的。

引用装配脑袋对这种情况的归纳:

//如果一个接口需要对T进行协变或反变,那么这个接口所有方法的返回值类型必须支持对T同样方向的协变或反变。
//这就是方法返回值的协变-反变一致原则。

以上仅仅是个人理解归纳,关于这个概念的理解,可以参看园子里的这两篇文章,写的非常详细。

装配脑袋的:

http://www.cnblogs.com/Ninputer/archive/2008/11/22/generic_covariant.html

还有这篇,用图的方式很好理解:

http://www.cnblogs.com/lemontea/archive/2013/02/17/2915065.html