协变还是逆变,这还是个问题吗
协变(Covariance)与逆变(Contravariance)是C#4.0的新特性, 初次接触逆变协变的很多人可能对这两个东西都感觉比较绕脑子,特别是逆变。
在讲述概念之前,我们先定义两个有继承关系的类:Fruit,Apple,Apple派生自Fruit。
{
public string Name { get; set; }
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的委托,这个过程就是逆变。
下面以泛型委托的协变逆变的运用来对上面概念作代码的实现。
委托中的协变
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当做实参传入,从而实现委托的协变。我们发现协变的实现思想和多态很相近。
委托中逆变:
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<Fruit> b = d;
接口中的逆变
Action<string> action2 = action1;//逆变
总结
1)仅有泛型接口和泛型委托支持对类型参数的协变或逆变,泛型类或泛型方法不支持;
2)值类型不参与协变或逆变;
3)通过逆变可以实现算法的复用;
4)不管是协变还是逆变,都是在类型安全的情况进行的;
5)协变对应返回类型,逆变对应参数类型;
参考文章:
Artech:C# 4.0新特性-"协变"与"逆变"以及背后的编程思想
装配脑袋:.NET 4.0中的泛型协变和反变