让返回值类型返回比声明的类型派生程度更大的类型,就是“协变”。协变不是一种新出现的技术,在以往的编码中,我们已经在不自觉地使用协变。以下的代码就是一个不自觉应用协变的例子:
public Employee GetAEmployee(string name)
{
Console.WriteLine("我是雇员:"+name);
return new Programmer(){Name=name};//Programmer是Employee的子类
}
Programmer是Employee的子类,所以一个Programmer对象也就是一个Employee对象。方法GetAEmployee返回一个Programmer的对象,也就相当于返回了一个Employee对象。正是因为在FCL 4.0以前的版本中,协变是如此自然的一种应用,所以,我们很有可能写出这样的代码:
class Program
{
static void Main(string[]args)
{
ISalary<Programmer>s=new BaseSalaryCounter<Programmer>();
PrintSalary(s);
}
static void PrintSalary(ISalary<Employee>s)
{
s.Pay();
}
}
interface ISalary<T>
{
void Pay();
}
class BaseSalaryCounter<T>:ISalary<T>
{
public void Pay()
{
Console.WriteLine("Pay base salary");
}
}
class Employee
{
public string Name{get;set;}
}
class Programmer:Employee
{
}
class Manager:Employee
{
}
在PrintSalary这个方法中,方法接收的参数类型是ISalary<Employee>。于是,我们想当然地认为ISalary<Programmer>必然也是可以被PrintSalary方法接收的。实际却不然,这段代码编译会发生类似如下的错误:
无法从"ConsoleApplication4.ISalary<ConsoleApplication4.Programmer>"
转换为"ConsoleApplication4.ISalary<ConsoleApplication4.Employee>"
编译器对于接口和委托类型参数的检查是非常严格的,除非用关键字out特别声明(在建议43中会阐述),不然这段代码只会编译失败。要让PrintSalary完成需求,我们可以使用泛型类型参数:
static void PrintSalary<T>(ISalary<T>s)
{
s.Pay();
}
注意 可能会有读者注意到,本建议开头处指出“协变”是针对返回值而言的,但是所举的这个例子却并没有体现“返回值”这个概念。实际上,只要泛型类型参数在一个接口声明中不被用来作为方法的输入参数,我们都可姑且把它看成是“返回值”类型的。所以,本建议中这种模式是满足“协变”的定义的。但是,只要将T作为输入参数,便不满足“协变”的定义了。如:
interface ISalary<out T>
{
void Pay(T t);
}
编译会提示:
方差无效:类型参数"T"必须为"ConsoleApplication4.ISalary<T>.Pay(T)"上有效的逆变式。"T"为协变。