1. 什么是协变和逆变
在C#编程语言中,有一些类型,可以从另一些类型中派生出来,我们称之为衍生类型。衍生类型之间的关系可以大致分为继承和实现接口两种方式,这意味着新类型具有跟父类型一样的成员,甚至可能会添加新的成员。在这些衍生类型间的转换时,可以发生的类型转换,有时我们可以称之为变化。
协变和逆变就是针对衍生类型之间的转换及其成员使用的变化。简单来说,协变就是将子类型转换为父类型,逆变就是将父类型转换为子类型。这两种转换在一些场合下由于涉及到泛型类型参数等信息,会比较严格而难以实现,所以在C#语言中,有专门的协变和逆变机制来帮助我们简化这个过程。
1.1 协变
协变被用于在子类型中返回父类型的方法上,它的状态可以从out
修饰变量的使用中来认识,对于可输出的变量,编译器会指示对应参数类型必须是协变的。这种返回结果类型兼容的场合下,可以将子类型的返回值直接赋给父类型,不过协变关系要求,这两个类型必须满足“类型成员存在某种继承关系”的要求。
例如下面的两个泛型类型定义:
class Parent{}
class Child : Parent{}
interface IProcess<out T>
{
T Get();
}
可以看到,IProcess<T>
中使用了T
类型参数,并应用了out
修饰,表示这个类型参数是支持协变的,下面我们将定义一个子类型并对其进行协变:
class Test
{
static void Main(string[] args)
{
IProcess<Child> childProcess = new ProcessImpl<Child>();
IProcess<Parent> parentProcess = childProcess;//进行协变
}
}
class ProcessImpl<T> : IProcess<T> where T : Parent
{
public T Get()
{
return default(T);
}
}
可以看到,我们先定义了一个类型为ProcessImpl<Child>
的对象childProcess
,它实现了接口IProcess<Child>
,因为IProcess<T>
类型参数用了out
修饰,所以这个接口支持协变方式,我们可以将这个对象强制转换为类型为IProcess<Parent>
的对象parentProcess
,这样就完成了协变转换。
1.2 逆变
逆变就是将父类型转换为子类型,它主要用于在参数上使用。与协变相似,逆变的状态可以由in
修饰的变量来表示,对于可输入的变量,编译器会要求对应参数类型必须是逆变的。这种参数类型兼容的场合下,可以将父类型的参数值直接传入子类型中。
例如下面的泛型类型定义:
class Parent{}
class Child : Parent{}
interface IProcess<in T>
{
void Add(T t);
}
可以看到,IProcess<T>
中使用了T
类型参数,并应用了in
修饰,表示这个类型参数是支持逆变的,下面我们将定义一个子类型并对其进行逆变:
class Test
{
static void Main(string[] args)
{
IProcess<Parent> parentProcess = new ProcessImpl<Parent>();
IProcess<Child> childProcess = parentProcess;//进行逆变,注意这里编译器会给出警告信息
}
}
class ProcessImpl<T> : IProcess<T> where T : Parent
{
public void Add(T t)
{
}
}
可以看到,我们先定义了一个类型为ProcessImpl<Parent>
的对象parentProcess
,它实现了接口IProcess<Parent>
,因为IProcess<T>
类型参数用了in
修饰,所以这个接口支持逆变方式,我们可以将这个对象强制转换为类型为IProcess<Child>
的对象childProcess
,这样就完成了逆变转换。
2. 什么时候使用协变和逆变
2.1 协变
协变通常应用于返回结果,返回子类型和返回父类型的关系是成对出现的,例如IEnumerable与IEnumerable<T>:
IEnumerable<object> objects = ...;
IEnumerable<string> strings = objects.Cast<string>();//将所有元素转化为string
在这个示例中,从IEnumerable<object>
到IEnumerable<string>
表示协变关系,Cast方法返回IEnumerable<TResult>
,这里的TResult与Cast参数类型是成对产生的。如果这里不支持协变,我们就必须使用Cast<string>方法作用于对象objects
之后得到一个中间对象,再将其显示强制转换,这样就多了一步操作,而协变则省去了中间步骤。
2.2 逆变
逆变应用就不太常见了,它通常用于接收参数。以下是一个用于比较两个对象大小的泛型程序,它可以比较整型、字符串、人类对象等等:
class Comparer<T>
{
public static bool Compare(T x, T y)
{
return x.Equals(y);
}
}
class Test
{
static void Main(string[] args)
{
int x1 = 1, x2 = 2;
Console.WriteLine(Comparer<int>.Compare(x2, x1));
string s1 = "hi", s2 = "world";
Console.WriteLine(Comparer<string>.Compare(s2, s1));
Human h1 = new Human { Name = "Tom", Age = 23 };
Human h2 = new Human { Name = "Jerry", Age = 20 };
Console.WriteLine(Comparer<Human>.Compare(h1, h2));
}
}
class Human
{
public string Name { get; set; }
public int Age { get; set; }
}
其中Comparer<T>.Compare(T x, T y)
表示比较两个对象,如果它们相等则返回true,否则返回false。但是上面的代码中有一个问题,比较器会将两个人类对象按照他们的名字进行比较,如果两个人的名字相同,那么依然会返回true,但是实际上我们通常是按照年龄大小来做比较的。
这个问题可以通过逆变来解决,在人类对象定义的部分增加比较器:
class HumanComparer : Comparer<Human>
{
public override bool Compare(Human x, Human y)
{
return x.Age > y.Age;
}
}
我们将比较器的逻辑放到了派生类型HumanComparer
中,它从基类型Comparer<Human>
派生而来,并重写了基类型中的比较方法Compare(T x, T y)
。同样在测试代码中稍作修改,以使用新的比较器对象:
class Test
{
static void Main(string[] args)
{
int x1 = 1, x2 = 2;
Console.WriteLine(Comparer<int>.Compare(x2, x1));
string s1 = "hi", s2 = "world";
Console.WriteLine(Comparer<string>.Compare(s2, s1));
Human h1 = new Human { Name = "Tom", Age = 23 };
Human h2 = new Human { Name = "Jerry", Age = 20 };
Console.WriteLine((new HumanComparer()).Compare(h1, h2));
}
}
至此,逆变就得以应用到了比较器中,HumanComparer
对象可以重载父类型中的Compare(T x, T y)
方法,此时在调用时传入的参数可以是父类型中定义的Comparer<Human>.Compare
要求的任何一个基类型。
3. 总结
本文介绍了C#编程语言中的协变和逆变机制,以及它们的应用场景。协变用于将子类型转换为父类型,逆变用于将父类型转换为子类型。我们重点讨论了协变和逆变在泛型类型中的应用,包括如何在泛型接口中使用协变和逆变,以及如何在自定义比较器中使用逆变。协变和逆变的应用可以在某些场合下简化代码,提高代码的可读性和可维护性。