C#中的协变、逆变
一、概述
1、在 C# 中,协变和逆变能够实现数组类型、委托类型和泛型类型参数的隐式引用转换。
2、按字面,可以这样理解:协变,感觉协和地隐式引用转换;逆变,逆反直觉地隐式引用转换。
1、定义
(1)协变,使你能够使用比原始指定的类型派生程度更大的类型。你可以将 IEnumerable<Derived> 的实例分配给 IEnumerable<Base> 类型的变量。
(2)逆变,使你能够使用比原始指定的类型更泛型的类型(派生程度更小的类型)。你可以将 Action<Base> 的实例分配给 Action<Derived> 类型的变量。
(3)不变性,表示只能使用最初指定的类型。 固定泛型类型参数既不是协变,也不是逆变。你无法将 List<Base> 的实例分配给 List<Derived> 类型的变量,反之亦然。
2、具有协变类型参数的泛型接口
(1)关键字 out
(2)在接口方法中,只能作为方法的返回类型
public interface IEnumerator<out T> : IEnumerator, IDisposable { T Current { get; } }
3、具有逆变类型参数的泛型接口
(1)关键字 in
(2)在接口方法中,只能作为方法的参数类型
public interface IComparer<in T> { int Compare(T? x, T? y); }
4、代码示例
(1)协变
internal partial class Program { private static void ToTest5() { ITest1<string> implementStr = new ImplementStr(); object result1 = implementStr.Foo(); Console.WriteLine(result1); ITest1<object> implementObj = implementStr; object result2 = implementObj.Foo(); Console.WriteLine(result2); } } public interface ITest1<out T> { T Foo(); } public partial class ImplementStr : ITest1<string> { public string Foo() { return " foo"; } } public partial class ImplementObj : ITest1<object> { public object Foo() { return 1; } }
运行结果:
(2)逆变
internal partial class Program { private static void ToTest6() { ITest2<object> implementObj = new ImplementObj(); implementObj.Bar("bar1"); ITest2<string> implementStr = implementObj; implementStr.Bar("bar2"); } } public interface ITest2<in T> { void Bar(T parameter); } public partial class ImplementStr : ITest2<string> { public void Bar(string parameter) { Console.WriteLine($" string parameter:{parameter}"); } } public partial class ImplementObj : ITest2<object> { public void Bar(object parameter) { Console.WriteLine($" object parameter:{parameter}"); } }
运行结果:
(3)示例说明
A、隐式引用转换
(A)协变中,隐式引用转换发生在 object result2 = implementObj.Foo(); 这个语句中。
- 它实际执行的方法是:public string Foo(),所以输出的是字符串 foo,而不是数字 1;
- 编译器对这个语句理解是,要求它返回 object 类型,通过鼠标停留,我们可以看到:
(B)逆变中,隐式引用转换发生在 implementStr.Bar("bar2"); 这个语句中。
- 参数是字符串 bar2,
- 调用的方法是 public void Bar(object parameter),所以输入的是 object parameter:bar2
B、协变、逆变名称来源
- 感觉协和的语句是:ITest1<object> implementObj = implementStr;
- 逆反直觉的语句是:ITest2<string> implementStr = implementObj;
前者将一个 ITest1<string> 实例赋值给了一个 ITest1<object> 变量,直觉上是子类对象赋值给父类变量,感觉协和的、融洽的;
后者将一个 ITest2<object> 实例赋值给了一个 ITest2<string> 变量,直觉上是父类对象赋值给子类变量,感觉逆反的、别扭的。
实际上,并没有发生“子类对象赋值给父类变量”和“父类对象赋值给子类变量”,发生的是“ ITest1 实例赋值给 ITest1 变量” 以及 “ITest2 实例赋值给 ITest2 变量”。
因为代码携带了实际的类型参数 object 或 string,才让开发者了产生了协和的或者逆反的感觉。
5、本质
本质都是遵循“里氏替换原则”,示例代码中,无论协变,还是逆变,都是 String 类型向 Object 类型的转换。
关键字 in 和 out 的使用,使编译器能够知道应该如何对类型参数进行“里氏替换原则”的检查,即类型安全检查。因此编译器拒绝这样的代码:
6、关键点
抓住“谁是变量,谁是实例”这个关键。“实例”的类型决定实际调用哪个方法的实现,“变量”决定返回什么类型(协变),或输入什么类型的参数(逆变)。