直接例子:
IEnumerable<string> strings = new List<string> { "a", "b", "c" };
IEnumerable<object> objects = strings; // No Error
IEnumerable转化里面,string转化为object没有任何问题,这在C#4.0之前是报错的。
IList<string> strings = new List<string> { "a", "b", "c" };
IList<object> objects = strings; // Error
IList转化里面,string转化为object报错,为什么?
namespace 协变
{
class Program
{
static void Main(string[] args)
{
IList<string> strings = new List<string> { "a", "b", "c" };
IList<object> objects = strings;
objects.Add(new object());
string element = strings[3];
}
}
}
先说结论,因为是不安全的。
按上述代码,一步一步看,当strings转化为object类型之后,objects再次添加一个新元素,这个元素是object类型,
添加成功了,但是这个元素就是完完整整的object类型。
strings[3]去取这个object是不会得到string的,因为这是错误的。
所以在编写代码的时候CLR检查就不让你通过。
可以按F12打开原生C#查看这些接口和类的定义,
可以发现:
IEnumerable只作为输出
IList同时作为输入和输出
Action只能输入
于是就有3种说法:
Covariance,Contravariance,Invariance
Covariance——协变
Contravariance——逆变
Invariance——不变
协变——作out输出——IEnumerable
逆变——作input输入——Action
不变——既是输出,又是输入——IList
不变的意思是,不能随便转换类型,如IList
协变可以理解为,子类的T可以转换父类T,
逆变正好反过来,Action object可以变成string
▢ public interface IEnumerable<out T>
▢ public delegate void Action<in T>
▢ public interface IList<T>
<out T>,协变,Covariance
<int T>,逆变,Contravariance
<T>,不变,Invariance
variance 转换
▢ variance只能出现在接口和委托里
▢ variance是引用转换的一个例子。
▢ 引用转换就是指,你无法改变其底层的值,只能改变编译时的类型。
换句话说,改变栈中引用的类型,但是不能改变对象在堆中内存位置。也就是不同类型的变量,保存的还是同一个指针。
▢ identity conversion,对CLR而言从一个类型转化到相同的类型
注意【对CLR而言】,你觉得在【C#层面】的不同类型,【在CLR眼中】是一样的。
合理的转换
▢ 如果从A到B的转换是本体转换或者隐式引用转换,那么从IEnumerable<A>到IEnumerable<B>的转换就是合理的:
▢ IEnumerable<string> to IEnumerable<object>
▢ IEnumerable<string> to IEnumerable<IConvertible>
▢ IEnumerable<IDisposable> to IEnumerable<object>
典型转换案例:
To:
Or:
不变→协变
IEnumerable这个协变,
List不变,
Cast方法实际上实现了IEnumerable且没有T,
Cast返回的类型其实就是IEnumerable<T>,它自己的T,
Or的方案可以打开ToList的元数据看,也是同样的道理。
性能提升Tips:
▢ C#的泛型,生产类型(例如List<T>)可以被编译到dll里。这是因为这种在生产者和产制封闭类型的消费者之间的合成是发生在运行时的。