C# 逆变(Contravariance)/协变(Covariance) - 个人的理解
逆变(Contravariance)/协变(Covariance)
1. 基本概念
官方: 协变和逆变都是术语,前者指能够使用比原始指定的派生类型的派生程度更大(更具体的)的类型,后者指能够使用比原始指定的派生类型的派生程度更小(不太具体的)的类型。协变/逆变只支持委托、泛型接口。数组由于历史的原因也支持协变/逆变。在 C# 中,协变和逆变能够实现数组类型、委托类型和泛型类型参数的隐式引用转换。 协变保留分配兼容性,逆变则与之相反。协变允许方法具有的返回类型比接口的泛型类型参数定义的返回类型的派生程度更大。只有引用类型才支持使用泛型接口中的变体。
协变:IFoo<父类> = IFoo<子类>;interface IFoo<out T>{}
逆变:IBar<子类> = IBar<父类>;interface IBar<in T>{}
以下代码演示分配兼容性、协变和逆变之间的差异。
string str = "test";
derived type.
object obj = str;
// Covariance.
IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings;
//static void SetObject(object o) { }
Action<object> actObject = SetObject;
Action<string> actString = actObject;
2、下面我谈谈个人对协变和逆变的理解
“协变”相当于隐式转换,基类集合=派生类集合,等于让基类集合指针指向派生类集合,基类集合可以用子类集合中方法。对于基类集合只是不能使用基类集合中属于子类的方法,基类集合没有性能上的损失,所以可以做回返回值。
“逆变”相当于强制转换,派生类集合=基类集合, 等于让派生类集合的指针指向基类集合,等于让派生类集合的指针指向基类集合。由于基类的功能小于派生类集合。就造成派生类可使用的方法减少,功能降低。所以不能作为返回值。
我们先来看一段代码:
List<汽车> 一群汽车 = new List<汽车>(); List<车子> 一群车子 = 一群汽车;
显然,上面那段代码是会报错的, 虽然汽车继承于车子,可以隐士转换为车子,但是List<汽车>并不继承于List<车子>,所以上面的转换,是行不通的。
IEnumerable<汽车> 一群汽车 = new List<汽车>(); IEnumerable<车子> 一群车子 = 一群汽车;
然而这样却是可以的。那么IEnumerable接口有什么不同呢,我们且看编译器的提示:
我们可以看到,泛型参数的,用了一个“out”关键字作为声明。看来,关键是这个在起作用了。我们先到这个为止,以上是抛出话题。
接下来我们看一种情况:
当尝试编译下面这个把in泛型参数用作方法返回值的泛型接口时:
public interface IPlayB<in T>
{
T Test();
}
出现了如下编译错误:
错误 1 方差无效: 类型参数“T”必须为“CovarianceAndContravariance.IPlayB<T>.Test()”上有效的 协变式。“T”为 逆变。
我们把in改成out ,就不会提示错误了,为什么会出现这种情况?
3. 自问自答
1)协变、逆变 为什么只能针对泛型接口或者委托?而不能针对泛型类?
因为它们都只能定义方法成员(接口不能定义字段),而方法成员在创建对象的时候是不涉及到对象内存分配的,所以它们是类型(内存)安全的。
2)协变、逆变 为什么是类型安全的?
本质上是里氏替换原则,由里氏替换原则可知:派生程度小的是派生程度大的子集,所以子类替换父类的位置整个程序功能都不会发生改变。
3)官方对 协变、逆变 的定义现在是否能看懂?
上面看懂了,官方定义肯定也是没问题的。派生程度小可以理解为基类,派生程度大可以理解为子类或派生类,至于为什么用程度这个词,是因为继承链的深度是没限制的。
4)为什么 in 、out 只能是单向的(输入或输出)?
因为若类型参数同时为输入参数和输出参数,则必然会有一种转换不符合里氏替换原则,即将父类型的变量赋值给子类型的变量,这是不安全的所以需要明确指定 in 或 out。
4、泛型接口类型变体
4.1协变允许方法具有的返回类型比接口的泛型类型参数定义的返回类型的派生程度更大。协变中泛型类型参数只能做 返回值类型,不能做方法的参数类型
不知道啥意思
4.2逆变允许方法具有的实参类型比接口的泛型形参定义的类型的派生程度更小。
不知道啥意思
4.3只有引用类型才支持使用泛型接口中的变体。 值类型不支持变体
IEnumerable<int> integers = new List<int>(); // The following statement generates a compiler error, . IEnumerable<Object> objects = integers; // 这个是错误的,因为int是值类型,
4、逆变和协变的应用
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; namespace Galaxy { class Program { static void Main(string[] args) { Dog aDog = new Dog(); Animal aAnimal = aDog; List<Dog> lstDogs = new List<Dog>(); // List<Animal> lstAnimal2 = lstDogs; IMyList<Dog> myDogs = new MyList<Dog>(); IMyList<Animal> myAnimals = myDogs; } } public interface IAnimal { public void AnimalRun(); } public abstract class Animal: IAnimal { public void AnimalRun() { Console.WriteLine("动物再跑"); } } public class Dog : Animal { } public interface IMyList<out T> { T GetElement(); } public class MyList<T> : IMyList<T> { public T GetElement() { return default(T); } } }
5、变体
如果泛型接口或委托的泛型参数被声明为协变或逆变,该泛型接口或委托则被称为“变体”。