.NET 4.0 “Covariance”和“Contravariance”趣话
面向对象的程序中,我们知道基类变量可以引用子类对象,比如List<T>派生自IEnumerable<T>,所以,以下这句绝无问题:
IEnumerable<Parent> P = new List<Parent>();
现在假设Parent类有一个子类,取名Child。
class Parent { }
class Child : Parent
{ }
请看以下“错误的”代码:
IEnumerable<Parent> P = new List<Child>();
虽然基类变量可以引用子类对象,但上述代码在.NET 4.0之前无法通过编译。
现在有趣的事情发生了,你会发现,同样的代码在.NET 4.0和Visual Studio 2010中则可以顺利通过编译。
这是怎么回事?
请看一下IEnumerable<T >的声明:
public interface IEnumerable<out T> : IEnumerable
{
IEnumerator<T> GetEnumerator();
}
这里面多了一个“神秘”的out关键字。正是因为它,才发生那些过去不可能发生的情况。
在.NET 4.0中,如果一个泛型接口(或泛型委托)的类型参数前有一个out关键字,那么,此类型参数“子类/父类对象通吃”。这种特性称为“Covariance”。
再来看.NET基类库中的泛型委托Action<T>的定义:
public delegate void Action< in T>(T obj);
请注意其中有一个“in”关键字,由于前面看到了“out”的介绍,敏感的读者一定会估计这个“in”里可能有点名堂,来看个例子。
以下代码定义了一个接收基类Parent对象的方法:
static void ParentFunc(Parent p)
{
//……(代码略)
}
我们发现,在.NET4.0中,此方法可以传给一个以子类Child作为类型参数的Action委托!
Action<Child> del = ParentFunc;
由此我们知道,在.NET 4.0中,如果一个泛型委托(或泛型接口)的类型参数前有一个in关键字,那么,“定义为子类型的in类型参数可以接收对应位置的定义为父类型参数的方法”,这句话实在是太别扭了,但请读者原谅我的汉语水平。
这种特性被称为“Contravariance”。
提示:
别问我“Covariance”和“Contravariance”和这两个词如何翻译,我也不知道,大家等着中文MSDN出来,看看微软的牛人们怎么将这两个词翻译为汉语吧。
为便于记忆,可以总结为两句话:
类型参数前有“in”的,基类可以传入给子类,叫“Contravariance”。
类型参数前有“out”的,子类可以传出给父类,叫“Covariance”。
比较“变态”的是有些委托可以同时“in”和“out”,请看.NET基类库中的Func<T,TResult>的定义:
public delegate TResult Func<in T, out TResult>(T arg);
于是,我们可以写出以下完全正确但却让人“昏菜”的代码:
Func<Child, Parent> func = MyFunc;
其中,MyFunc方法定义如下:
static Child MyFunc(Parent p)
{
return p as Child;
}
警告:
在实际开发中别写这样的代码,如果你这么做了,我担保你一定会被需要维护你代码的同事痛扁一顿!
依据上述原则,你完全可以使用in和out关键字定义支持“Covariance”和“Contravariance”特性的泛型接口与泛型委托。
不过,如果没有特殊需求,你还是直接用基类库中的现有接口和委托就行了,不要滥用“Covariance”和“Contravariance”特性。
对了,如果一个泛型接口(或泛型委托)声明为支持“Covariance”或“Contravariance”特性,我们就将它们统称为“variant”的泛型接口(或泛型委托)。
事实上,Covariance和Contravariance不仅适用于泛型接口和泛型委托,同样适用于非泛型的委托。
例如,以下代码定义了一个MyDelegate委托,注意它返回一个Parent对象:
public delegate Parent MyDelegate();
则我们可以写出以下代码:
MyDelegate del = delegate()
{
return new Child();
};
需要注意的是,上述使用“匿名方法”给委托变量赋值仅适用于“Covariance”,如果要用于“Contravariance”,你必须老老实实地写一个独立的函数,再赋值给委托变量。
好了,来看一个“真正有点用”的实例吧。
请看一个“使用同一个函数同时响应鼠标和键盘操作”的示例程序ContravarianceExample:
示例程序定义了一个键盘和鼠标事件响应函数:
private void MultiHandler(object sender, System.EventArgs e)
{
if (e is KeyEventArgs)
lblInfo.Text = string.Format("您敲了{0}键",
(e as KeyEventArgs).KeyCode.ToString());
if(e is MouseEventArgs)
lblInfo.Text = "您按了鼠标上的键";
}
示例程序的奇特之处在于,此函数可以直接挂接到按钮的KeyDown和MouseClick事件上!
btnTestCovariance.MouseClick += MultiHandler;
btnTestCovariance.KeyDown += MultiHandler;
让我们分析一下“后台”到底发生了什么事情。
首先,我们注意到MouseClick事件和KeyDown事件的参数拥有以下继承关系:
再来看一下KeyDown事件和MouseClick事件的定义:
public event KeyEventHandler KeyDown;
public event MouseEventHandler MouseClick;
请注意两个事件其实都是委托类型的变量。以下是两个事件委托的定义:
public delegate void KeyEventHandler(object sender, KeyEventArgs e);
public delegate void MouseEventHandler(object sender, MouseEventArgs e)
可以看到,这两个委托的定义,其参数都是EventArgs的子类。所以,依据Contravariance特性,它们可以接收参数定义为父类EventArgs的方法。这正是MultiHandler可以直接作为统一的事件响应函数挂接到键盘和鼠标事件的原因。
===================
下载示例程序(http://files.cnblogs.com/bitfan/ContravarianceExample.rar)