天天看点

C#协变和抗变

本文为了表述清楚,一些说法不是很规范。比如,把方法赋值给委托,意思是,用这个方法作为委托构造函数的参数。

MSDN里面有句话是:

.NET Framework 3.5 and Visual Studio 2008 introduced variance support for matching method signatures with delegate types in all delegates in C# and Visual Basic. This means that you can assign to delegates not only methods that have matching signatures, but also methods that return more derived types (covariance) or that accept parameters that have less derived types (contravariance) than that specified by the delegate type.This includes both generic and non-generic delegates.

来自 http://msdn.microsoft.com/en-us/library/dd233060.aspx

意思是,如果定义一个delegate,那么不仅仅签名完全相同的方法可以赋值给delegate变量。

如果一个方法的参数表符合delegate声明,但返回的类型是 (delegate声明返回类型) 的派生类,那也可以将这个方法赋值给这个delegate变量。

如果一个方法的返回类型符合delegate的声明,但参数是(delegate声明参数类型)的祖先类,那也可以将这个方法赋值给这个delegate变量。

如果一个方法的参数和返回类型都符合上面两行的假设,那也可以将这个方法赋值给这个delegate变量。

实例代码如下:

class Program
    {
        public delegate Base TestDelegate(Derived d);             //声明一个delegate

        public static Base Test1(Derived d)                       //这个方法符合delegate的签名和返回类型
        {
            Console.WriteLine(d);
            return new Base();
        }

        public static Derived Test2(Derived d)                    //这个方法的返回类型是Base的派生类
        {
            Console.WriteLine(d);
            return new Derived("C");
        }

        public static Derived Test3(Base b)                       //这个方法的参数是Derived的基类,返回类型是Base的派生类
        {
            Console.WriteLine(b);
            return new Derived("D");
        }

        static void Main(string[] args)
        {

            TestDelegate td;
            //Case 1

 td = new TestDelegate(Test1); //OK
            Base b1 = td(new Derived("E"));
            Console.WriteLine(b1.GetType()); //Variance.Base
//Case 2
            td = new TestDelegate(Test2); //OK
            Base b2 = td(new Derived("F"));
            Console.WriteLine(b2.GetType()); //Variance.Derived
//Case 3
            td = new TestDelegate(Test3); //OK
            Base b3 = td(new Derived("G"));
            Console.WriteLine(b3.GetType()); //Variance.Derived } }
           

1 为什么返回类型可以是Base的派生类?

假如返回类型是Base的祖先类,那么用td调用的返回类型将是Base的祖先类。而我们只知道td这个delegate会返回Base,所以会把td的返回结果赋值给Base变量。返回祖先类的实例将无法赋值给Base变量。

但是如果返回类型是Base的派生类,那么用td调用的返回结果赋值给Base变量将没有任何问题!

所以如果返回类型如果是Base的派生类,那么这个方法就可以赋值给委托。

就像上面的Case2和Case3。

2 为什么参数类型可以是Derived的祖先类?

假如一个方法的参数是Derived的祖先类,说明在这个方法里面对Derived的祖先类进行了处理。

那么将这个方法赋值给委托将不会有任何问题。为什么?只要保证通过这个委托调用该方法不会造成任何错误就行。

因为我们使用这个委托时,只能传递Derived或Derived的派生类的实例作为参数。既然实际上这个方法实际参数是Derived的祖先类,那么传递Derived或Derived的派生类的实例将不会有任何问题。

所以参数类型如果是Derived的祖先类,这个方法就可以赋值给委托。

就像上面的Case3。

3 如果返回类型满足情形1,参数类型满足情形2,那当然也可以。就上上面的Case3。

对泛型委托:

public delegate T1 TestDelegate2<T1, T2>(T2 t);                       //这里没有把T1声明为协变,没有把T2声明为抗变

            TestDelegate2<Base,Derived> td;

            td = new TestDelegate2<Base, Derived>(Test1);             //OK
            Base b1 = td(new Derived("E"));
            Console.WriteLine(b1.GetType());

            td = new TestDelegate2<Base, Derived>(Test2);             //OK
            Base b2 = td(new Derived("F"));
            Console.WriteLine(b2.GetType());

            td = new TestDelegate2<Base, Derived>(Test3);             //OK
            Base b3 = td(new Derived("G"));
            Console.WriteLine(b3.GetType());
           

上面没啥问题,是直接把方法赋值给委托。跟上面的代码一个意思。

但问题在下面:

TestDelegate2<Derived, Derived> td1;
            td1 = new TestDelegate2<Derived, Derived>(Test2);
            td = td1;
           

用Test2直接赋值给td是可以的,为啥经过td1再赋值给td就不行。

这是因为td是TestDelegate2<Base, Derived>类型,而td1是TestDelegate2<Derived, Derived>,不存在TestDelegate2<Derived, Derived>到TestDelegate2<Base, Derived>的类型转换。

为了解决上面的问题,要把T1声明为协变,T2声明为抗变,这样就没问题了。

public delegate T1 TestDelegate2<out T1, in T2>(T2 t);
           

协变和抗变只能用在引用类型上面,不能用在值类型上面。

协变和抗变只能用在泛型委托和泛型接口上面。

If a method of an interface has a parameter that is a generic delegate type, a covariant type parameter of the interface type can be used to specify a contravariant type parameter of the delegate type.

继续阅读