协变和逆变以及不变都是基于引用类型来讲的,我们一步一步的说起。
首先,面向对象设计有一个准则是“里氏替换原则”,这个原则是在讲“基类出现的地方,可以用子类来替换。”处于演示的目的,我们创建两个类,并让一个类继承于另一个类。让我们通过C#的例子来看一下:
public class BaseClass
{ //基类
// 处于简化的目的,这里并没有任何实现
}
public class DerivedClass : BaseClass
{
//子类
}
.....................
static void Main(string[] args)
{
DerivedClass derivedClass=new DerivedClass();
BaseClass baseClass = Test(derivedClass);
Console.ReadLine();
}
public static BaseClass Test(BaseClass baseClass)
{
return new DerivedClass();
}
在这个例子总,我们定义了一个Test方法,该方法接受一个BaseCLass类型的参数,输出一个DerivedClass类型的值。
先看一下这个方法的输出类型:是一个BaseClass,我们可以在方法内部返回一个DerivedClass,编译器会正常通过,说明实现的类型转换是安全的,实际上就是安全的:我们用一个BaseClass类型的变量去引用该方法的返回值,这个是一个安全的向上转换。这个是叫做输出类型的协变性。在来看一下该方法的输入参数类型:是一个BaseClass,我们可以在调用这个方法的时候放进去一个子类,编译器也不会报错,也是一种安全的类型向上转换。这个是叫做输入类型的逆变性,协变性和逆变性本质上都是符合“里氏替换原则”的。但是在C#中,对于协变和逆变的支持是有限的,比如,我们定义一个接口,然后用一个类来实现这个接口:
//缺乏协变的返回类型
public interface ICloneable{
object Clone();
}
我们如果要定义一个类来实现这个接口,比如我们定义一个Person类:
public class Person:ICloneable{
public Person Clone(){
//编译器会提示未实现接口方法
}
}
我们定义的这个Person类实现的Clone方法想返回一个Person,这个本来是很符合逻辑的一件事,但是C#不允许我们这么做。参数也存在类似的问题。 假定一个接口方法或一个虚方法, 其签名是void Process( string x),那么在实现或者覆盖这个方法时, 使用一个放宽 了 限制的签名应该是合乎逻辑的, 如 void Process( object x)。 (之所以合乎逻辑,是因为可以在要求输入基类的输入参数位置放进去一个子类)这称为参数类型的逆变性。关于解决的方案可以在C# in depth中查找。这里因为篇幅的原因,略去。
委托上的协变和逆变
逆变:委托的逆变可以通过windows forms程序来看:
public delegate void EventHandler( object sender, EventArgs e)
public delegate void KeyPressEventHandler( object sender, KeyPressEventArgs e)
public delegate void MouseEventHandler( object sender, MouseEventArgs e)
这三个委托在windows forms中用来定义一些事件。他们包含的参数中的第二个参数都是继承自EventArgs类的,我们定义如下方法:
public static void LogPlainEvent(object sender,EventArgs e){
Console.WriteLine("Event ocuured!");
}
接下来,我们就可以将这个事件处理方法注册到相关事件上:
Button button = new Button();
button. Text = "Click me";
button. Click += LogPlainEvent; //❶ 使用方法组转换
button. KeyPress += LogPlainEvent; //❷ 使用转换和逆变性
button. MouseClick += LogPlainEvent;
可以看到,委托是可以享受到逆变带来的便利的。
接下来我们看一下委托的协变:
public delegate Stream StreamFactory(); //❶ 声明 返回 Stream 的 委托 类型 static MemoryStream GenerateSampleData() //❷ 声明 返回 MemoryStream 的 方法
{
byte[] buffer = new byte[ 16];
for (int i = 0; i < buffer. Length; i++)
{
buffer[i]=(byte)i;}
return new MemoryStream( buffer); }
...
StreamFactory factory = GenerateSampleData; //❸ 利用 协 变性 来 转换 方法 组
using (Stream stream = factory()) //❹ 调用 委托 以 获得 Stream
{ int data;
while ((data = stream. ReadByte()) != -1)
{ Console. WriteLine( data); }
}
可以看到返回类型为MemoryStream的方法组可以转换为返回类型为Stream的StreamFactory委托。
C#4及以后的委托
内建的Action<in T,....in Tn>
和Func<in T,out TResult>
是在C#4出现的,包括泛型接口和泛型委托,这种以in和out来定义的限制的协变和逆变。使用情况和上面介绍的一样。以上面定义的BaseClass和DerivedClass来举例的话,Action<DerivedClass>=Action<BaseClass>
体现的是逆变,Func<DerivedClass,BaseClass>=Func<BaseClass,DerivedClass>
体现的是输入类型的逆变和输出类型的协变。但这和真正的逆变和协变不是一回事。
C#还没有实现。