协变还是逆变,这还是个问题吗

     

       协变(Covariance)与逆变(Contravariance)是C#4.0的新特性, 初次接触逆变协变的很多人可能对这两个东西都感觉比较绕脑子,特别是逆变。

      在讲述概念之前,我们先定义两个有继承关系的类:Fruit,Apple,Apple派生自Fruit。

 

 public class Fruit
    {
        
public string Name { getset; }
        
public override string ToString()
        {
            
return Name;
        }
        
public virtual void GetName()
        {
            Console.WriteLine(
"Fruit:{0}",Name);
        }


    }

    
public class Apple:Fruit
    {
        
public override string ToString()
        {
            
return base.Name;
        }

        
public override void GetName()
        {
            Console.WriteLine(
"Apple:{0}", Name);
        }
    }

 

      根据继承和多态的特性,我们知道,Fruit类型变量可以指向Apple实例:

      Fruit fruit = new Apple();

      仅有泛型接口和泛型委托支持对类型参数的协变或逆变,泛型类或泛型方法不支持, 如果泛型接口或泛型委托的泛型参数声明为协变或逆变,则将该泛型接口或委托称为“变体”。

      协变:能够使用与原始指定的派生类型相比派生程度更大的类型。如我在一个委托中定义的返回值类型为Fruit 类型,按常理我们定义的委托方法的返回类型也只能是,Fruit但通过协变的委托变体,在使用这个委托变量的地方我们可以使用定义返回类型为Apple的方法,这个过程就是协变。

      逆变:能够使用与原始指定的派生类型相比派生程度更小的类型。如定义的方法参数为泛型类型为Apple的委托,但我在调用这个方法的时候可以传入泛型类型为Fruit的委托,这个过程就是逆变。  

 

      下面以泛型委托的协变逆变的运用来对上面概念作代码的实现。

 

      委托中的协变

 

    public delegate T Function<out T>();
    
class Program
    {
       
        
static void Main(string[] args)
        {
            Test(GetApple);
            Test(GetFruit);
            Console.ReadKey();

        }

        
static void Test(Function<Fruit> function)
        {
            Fruit fruit
= function();
            fruit.GetName();
        }

        
static Apple GetApple()
        {
            
return new Apple(){Name = "苹果"};
        }
        
        
static Fruit GetFruit()
        {
            
return new Fruit(){Name = "水果"};
        }
    }

 

      输出结果如下:

      Apple:苹果

      Fruit:水果

 

      public delegate T Function<out T>(),这是定义一个可以协变的泛型委托:out关键字表明将返回类型T声明为协变。out关键字是对应于委托中的返回类型的,如果是参数类型则使用关键字in。

      Test(Function<Fruit> function)方法中,参数类型是Function<Fruit>,但我们定义的委托的Function是可协变的,所以我们调用Test方法时,可以把和泛型类型为Fruit子类的Apple的委托Function<Apple>匹配的方法GetApple当做实参传入,从而实现委托的协变。我们发现协变的实现思想和多态很相近。

 

       委托中逆变:

 

public delegate void Function2<in T>(T t);
    
class Program
    {
       
        
static void Main(string[] args)
        {
            Test2(GetApple2);
            Console.ReadKey();

        }

        
static void GetApple2(Fruit f)
        {
            f.GetName();
        }
        
static void Test2(Function2<Apple> function2)
        {
            function2(
new Apple() { Name = "苹果" });
        }
    }

 

       输出结果为:

       Apple:苹果

 

        public delegate void Function2<in T>(T t);定义一个能够对T类型逆变的泛型委托。

        Test2(Function2<Apple> function2) 方法中我们声明的参数类型为Function2<Apple>,但我们调用Test2方法的时候传入的是和泛型类型为Apple父类Fruit类型的委托Function2<Fruit>匹配的方法GetApple2(Fruit f),由此实现泛型委托的逆变。

 

       我们发现从C#2.0开始,委托就支持协变和逆变,但不支持关键字in和out,在上面泛型委托的协变逆变演示代码中,我们去掉关键字in和out,程序还是能正常运行。但在C#4.0之前,泛型接口是不支持协变逆变的,因为当时还没有在泛型中引入in和out关键字(如果泛型类型T使用in,则T类型只能用于方法的参数,使用out,则T类型只有用作方法的返回类型),一个泛型接口中定义的方法返回类型和参数类型都可以是泛型类T,支持协变逆变就无法保证类型安全,.NET4.0后,使用in和out关键字,限制了接口中方法参数和返回类型,类型安全得到了确保,所以泛型接口可以支持协变逆变,于是很多接口在.NET 4.0进行了升级,如IEnumerable<T>重新声明为IEnumerable<out T>。

 

      接口中的协变

 

 IEnumerable<Apple> d = new List<Apple>() { new Apple() { Name = "Apple1" }, new Apple() { Name = "Apple2" } };
            IEnumerable
<Fruit> b = d;

 

      接口中的逆变

 

      Action<object> action1 = o => { };
      Action
<string> action2 = action1;//逆变

    

      总结

      1)仅有泛型接口和泛型委托支持对类型参数的协变或逆变,泛型类或泛型方法不支持;

      2)值类型不参与协变或逆变;

      3)通过逆变可以实现算法的复用;

      4)不管是协变还是逆变,都是在类型安全的情况进行的;

      5)协变对应返回类型,逆变对应参数类型;

 

      参考文章:

      Artech:C# 4.0新特性-"协变"与"逆变"以及背后的编程思想

      装配脑袋:.NET 4.0中的泛型协变和反变

posted on 2011-08-07 22:05  边写边唱  阅读(2567)  评论(5编辑  收藏  举报

导航