C# 中的协变和逆变

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#编程语言中的协变和逆变机制,以及它们的应用场景。协变用于将子类型转换为父类型,逆变用于将父类型转换为子类型。我们重点讨论了协变和逆变在泛型类型中的应用,包括如何在泛型接口中使用协变和逆变,以及如何在自定义比较器中使用逆变。协变和逆变的应用可以在某些场合下简化代码,提高代码的可读性和可维护性。

后端开发标签