Covariance and Contravariance in Generics(泛型中的协变和逆变)

http://msdn.microsoft.com/zh-cn/library/dd799517.aspx


.NET 4泛型中的Covariance和Contravariance(http://www.2cto.com/kf/201109/106331.html)

在介绍.NET 4泛型的covariance和contravariance之前,先介绍下编程语言类型系统的variance的概念。简单的说covariance使得你能够用一个更具体的类来替代一个本该父层的类。在C#中,引用类型的数组是covariant的,这是从当时的Java中学来的特性,例如:

namespace variance2
{
    public class Animal { }
    public class Dog : Animal { }
    class Program
    {
        static void Main(string[] args)
        {
            Animal[] animals = new Dog[10];
            animals[0] = new Animal();
        }
    }
}当给animals数组初始化的时候,可以使用它的子类Dog,这里是covariance的。这段代码可以编译通过,但是运行的时候会报如下错误:

Attempted to access an element as a type incompatible with the array.

因为一旦给数组赋值了Dog,实际上它就是一个Dog数组,所以不能再给他赋值Animal。一段能编译通过的代码再运行时发生类型错误并不是好事情,所以在.Net 2引入泛型的时候.net的泛型是invariant的。

List listAnimals = new List();这样是不能通过编译的。

List listDogs = new List();
listDogs.Add(new Animal());这样也不能通过编译。.NET2 这样做避免了一些问题,但同时也带来了一些问题,例如: List listDogs = new List();
IEnumerable enumAnimals = listDogs;这样是不能编译通过的。实际上,这种编程场景很常见,而且事实上这是类型安全的,因为通过IEnumerable接口,我们只能从enumAnimals中获取值,而不能给他赋值。

 

下面举一个contravariance的例子:

public class Animal
   {
       public int Weight
       {
           get;set;
       }

       public string Name
       {
           get;set;
       }

       public Animal(string name,int weight)
       {
           Name=name; Weight=weight;
       }
   }
   public class Dog : Animal
   {
       public Dog(string name,int weight):base(name,weight)
       {
       }
   }

   public class WeightComparer : IComparer
   {
       public int Compare(Animal x, Animal y)
       {
           return x.Weight - y.Weight;
       }
   }给动物类加上一个重量的属性,并且实现一个根据重量排序的IComparer类。

class Program
{
    static void Main(string[] args)
    {
        WeightComparer comparer = new WeightComparer();
        List animals = new List();
        animals.Add(new Animal("Dog", 4));
        animals.Add(new Animal("Mouse", 1));
        animals.Add(new Animal("Tiger",44));
        animals.Sort(comparer);   //works fine
        animals.ForEach(e=>Console.WriteLine(e.Name+" "+e.Weight));

        List dogs = new List();
        dogs.Add(new Dog("DogA", 12));
        dogs.Add(new Dog("DogB", 10));
        dogs.Sort(comparer);     //compile error
        dogs.ForEach(e => Console.WriteLine(e.Name + " " + e.Weight));
    }
}注意如果在.net 2中运行,第一段程序是可以正常运行的。第二段程序会导致编译错误,这里本应该的对象是 IComparer,但是实际上传给他的是ICompaer,是contravariance,这在.net 2中是不允许的,实际上,这种情况也是类型安全的。

 

.Net 4中在泛型参数前可以加上in或者out关键字。

in 关键字可以使参数变成contravariant的。(子类可以用父类进行替换,如:Dog可以用Animal进行)

out 关键字可以使参数变成covariant的。(父类可以由子类进行替换,如:Animal 可以用Dog 进行)

 

在.net 类库中,很多泛型接口和委托的声明已经改变。例如,使用out关键字的有:

IEnumerable(Of T), IEnumerator(Of T), IQueryable(Of T) IGrouping(Of TKey, TElement)

使用in关键字的有:

IComparer(Of T), IComparable(Of T), IEqualityComparer(Of T).

因此,上文中的涉及到泛型接口的代码在.NET4中都是可以运行成功的。

 

.Net4中还有一些常用的委托也使用了in,out关键字来声明泛型参数。例如:

Action 委托,其声明为:

public delegate void Action( T obj )
可以使用contravariance:

static void Main(string[] args)
{
    Action animal = (obj) => { Console.WriteLine(obj.GetType().ToString()); };
    Action dog = animal;
    dog(new Dog("Test",1));
}再例如,Func委托,声明为:

public delegate TResult Func( T arg )

传入参数可以是contravariant,返回值可以是covariant的。

public class Type1 {}
public class Type2 : Type1 {}
public class Type3 : Type2 {}

public class Program
{
    public static Type3 MyMethod(Type1 t)
    {
        return t as Type3 ?? new Type3();
    }

    static void Main()
    {
        Func f1 = MyMethod;

        // Covariant return type and contravariant parameter type.
        Func f2 = f1;
        Type1 t1 = f2(new Type3());
        Console.WriteLine(t1.GetType());
    }
}

运行结果是 Type3.


一句话清晰总结协变(covariant)和逆变 (contravariant)

( http://dotnet.cnblogs.com/page/121058/)

看到过园子里面几篇协变和逆变的文章,但是总觉得写得不够清晰,文章这东西最重要的是要把自己想表达的观点表达出来,这个过程应该是把复杂的东西消化出来从而简单化,清晰化,而不是故弄玄虚,反其道而行之,下面我们言归正传啦。

  我们先来看一段MSDN原文给协变,逆变和变体下个定义:

A generic interface or delegate is called variant if its generic parameters are declared covariant or contravariant.Both C# and Visual Basic enable you to create your own variant interfaces and delegates.

  如果泛型接口或委托的泛型参数声明为协变或逆变,则将该泛型接口或委托称为“变体”。 C# 和 Visual Basic 都允许您创建自己的变体接口和委托。

  通俗解释:

  变体定义 - 带有协变或逆变参数的泛型接口或委托。也就是说协变和逆变主要关注点在泛型接口或委托。

  那什么又是协变和逆变呢?

  协变

  我们先来看下面一个来自MSDN的例子:

// 协变
IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings;
//大家看到了么一个声明为IEnumerable的接口类型被赋给了一个更低级别的IEnumerable.
//对,这就是协变。再来看一个例子:
class Base
{
    public static void PrintBases(IEnumerable bases)
    {
        foreach(Base b in bases)
        {
            Console.WriteLine(b);
        }

    }
} 

class Derived : Base
{
    public static void Main()
    {
        List dlist = new List(); 
        Derived.PrintBases(dlist);
     //由于IEnumerable接口是协变的,所以PrintBases(IEnumerable bases)
        //可以接收一个更加具体化的IEnumerable作为其参数。
        IEnumerable bIEnum = dlist;
    }
} 
   
  

  下面给协变下个定义:

  协变:让一个带有协变参数的泛型接口(或委托)可以接收类型更加精细化,具体化的泛型接口(或委托)作为参数,可以看成OO中多态的一个延伸。

  逆变

//  逆变
//  Assume that the following method is in the class: 
//  static void SetObject(object o) { } 
Action< object> actObject = SetObject;
Action< string> actString = actObject; 
// 委托actString中以后要使用更加精细化的类型string不能再使用object啦!
string strHello(“Hello”); 
actString(strHello);

  大家看到了么?一个声明为Action的类型被赋给了一个Action,大家都知道,Action接收参数,没有返回值,所以其中的object和string是其参数,这个过程其实就是参数的约束更加强了,也就是说让参数类型更加精细化。下面我们来给逆变下个定义:

  逆变:让一个带有协变参数的泛型接口(或委托)可以接收粒度更粗的泛型接口或委托作为参数,这个过程实际上是参数类型更加精细化的过程。

  总结

  一句话总结:协变让一个粗粒度接口(或委托)可以接收一个更加具体的接口(或委托)作为参数(或返回值);逆变让一个接口(或委托)的参数类型(或返回值)类型更加具体化,也就是参数类型更强,更明确。

  通常,协变类型参数可用作委托的返回类型,而逆变类型参数可用作参数类型。对于接口,协变类型参数可用作接口的方法的返回类型,而逆变类型参数可用作接口的方法的参数类型。


你可能感兴趣的:(C#)