C#4.0和VS2010新特性(二)

时间:2022-06-20 00:39:22

6)协变和反变(Co-variant & Crop-variant)

这是VS2010新增的一个内容,用于在编译的时候确认是否允许不同类型的泛型接口之间是否存在转换的问题。

为了了解“协变”和“反变”的概念,我们先看一个例子:

假设我们定义了一个接口和若干类:

class Father

    {

        public virtual void Say()

        {

            Console.WriteLine("Father");

        }

    }

 

    class Son : Father

    {

        public override void Say()

        {

            Console.WriteLine("Son");

        }

    }

   class Son2 : Father

    {

        public override void Say()

        {

            Console.WriteLine("Son2");

        }

    }

Interface  InAndOut<T, V>

             {

             void Input(V value);

            T Output();

}

class Program<T,V>:InAndOut<T,V>

    {

        private object value = default(V);

 

        public T Output()

        {

            return (T)value;

        }

        public void Input(V tv)

        {

            value = tv;

        }

    }

又假如我们已经实例化两个接口的实例:

InAndOut<Father, Son> iobj1 = new Program < Father, Son >();

InAndOut<Son, Father> iobj2 = new Program < Son, Father >();

 

现在我们令:iobj1= iobj2,可行吗?

 

            乍一看似乎可行——为什么呢?因为联想到右边的两个子类Son会被自动隐式转化成其父类Father。就好像是Father f = new Son()一样可以(注意:我们把子类隐式转化成父类成为“协变”,反之成为“反变”)。而且,根据接口定义,输入方向是接受一个Son(实际存储在iobj2中,被隐式转成Father),输出的时候还是存储的Son被隐式转化成Father输出。

            这种思考逻辑固然没有错,但是它有一个前提条件——即从iobj1输入方向看,必须是Son到Father,输出的话也必须是Son到Father!但是泛型仅仅是一个定义,谁能够保证在类中Father或者Son一定是输入(或者是输出)参数呢?如果我们改变成以下的形式呢?

class Program<T,V>:InAndOut<T,V>

    {

        private object value = default(T);

 

        public V Output()

        {

            return (V) value;

        }

        public void Input(T tv)

        {

            value = tv;

        }

    }

            这样就出现了问题——首先,iobj1指向iobj2(接受一个Father的参数,此时如果我的Father输入的是Son2,那么实际转化到Son的时候就发生异常了;同样地,输出因为是Son2不能转化成Son,因此发生异常)。

这个问题就是因为输入输出方向不明确所导致的。如果我们强制是一开始就给出的输入(输出方向)。即V只能作为输入,T只能作为输出就可以了。

推而广之,假设存在两个泛型TV,假设VTVT的子类)。那么泛型接口之间转换的一般规则是:输出类型是父类,“输出”一般是协变;反之,输入类型是子类,一般是反变。约束这种输入输出泛型的规则就是:输出线的接口加关键词out,输入加in如下所示:

Interface  InAndOut<out T, in V>

             {

             void Input(V value);

            T Output();

}

            那么你输入以下代码,就可以输出结果:

InAndOut<Father, Son> iobj1 = new Program < Father, Son >();

InAndOut<Son, Father> iobj2 = new Program < Son, Father >();

            这种规则本质上是编译器的行为理解。但是不一定就是正确结果,考察下列例子:

InAndOut<Father, Son> iobj1 = new Program < Father, Son >();

InAndOut<Son2, Father> iobj2 = new Program < Son2, Father >();

这一段代码照样可以通过编译,但是运行仍旧报异常。为什么呢?因为“输入端”和“输出端”尽管都符合了隐式转换的条件,但是你注意:把一个Son对象存储到iobj2的时候,iboj2的输出要求是Son2,而不是Son! 因此要保证运行正确,必须做到这样:

InAndOut<Father, Son> iobj1 = new Program < Father, Son >();

InAndOut<Son, Father> iobj2 = new Program < Son, Father >();

 

输入端接受Son,能够隐式转成蓝色的Father,蓝色Father存储的子类对象同样必须可以转化成Son(即一个协变的东西必须能够支持其反变;反之,一个反变的泛型必须支持其协变泛型,这就是著名的“协变反变类型”,简称“协-反变类型”)。

 

证明:(红色的Son开始):隐式转化成Father(协变),然后蓝色的Father(其中存储Son)强制转化成绿色的Son(反变)。

同理,(黑色的Father开始):黑色Father返回的内容(存储Son)可以强制转化成蓝色Father的内容(反变),同时可以隐式转化成绿色Son(协变)。我觉得可以使用对角线规则验证(猜想,对于任意的泛型A,B,C,D):

            InAndOut<A,B>

                                                (B既可以转成D,也可以转成C;输出A包含的内容

                                                   可以转化成D,也可以转化成C)

            InAndOut<C, D>