本文参照书籍《深入理解 C#》
- 泛型的协变: 往更大的基类转换,例如string转object。泛型值只用作输出
public interface IEnumerable<out T>
- 泛型的逆变: 往更小的派生类转换。例如object转string。泛型值只用作输入(接收)
public delegate void Action<in T>
- 泛型的不变: 既用做输入,又用作输出的时候
public interface IList<T>
泛型变体的主要区别是数据的流向!因为只能用在接口和委托上,所以下面的例子涵盖了所有范例。
只有转换的时候存在隐式转换或一致性转换时才能生效。
// 正常
IEnumerable<string> strings = new List<string>{"a", "b", "c"};
IEnumerable<object> objects = strings;
// IEnumerable的定义如下,意味着IEnumerable可以接收object,而string当然是object。
// public interface IEnumerable<out T>
// IEnumerable<object> 意味着可以输出成object对象。而string当然是可以输出成object对象的。
// IEnumerable<string> 转换成 IEnumerable<object> 是隐式转换。把string转成object。
// 而如果是object转string,那是显式转换,会报错。
Action<object> objectAction = obj=> Console.WriteLine(obj);
Action<string> stringAction = objectAction;
stringAction("pp");
// 正常
// Action定义如下
// public delegate void Action<in T>(T obj);
// Action<object> 意味着接收一个object类型的参数
// 而Action<string>接收一个string类型。一个action可以接收object类型,那肯定可以接收string类型参数
// 编译器告警
IList<string> strings2 = new List<string>{"a", "b", "c"};
IList<object> objects2 = strings;
// IList定义如下
// public interface IList<T>
// IList<T>为什么会告警?
// IList<T>是既用作输入,又用作输出。那么就不能从string转换成object,反向也是一样。
// 也就是说原来是什么样,接收也要是什么样。
// 因为objects2实际上丢失掉了类型信息,编译器为了保证类型安全,不允许这样的操作。
// IList是有add方法的,那么我们就可以添加进去一个object。
// 可是再次使用 string a = strings[2]的时候,会与你的add语句冲突,导致类型不安全。
// 很明显输入和输出是同样的类型,并没有转换。把类型形参用作输入,却声明为协变(只用做输出)。
public delegate void InvalidCovariant<out T>(T input)
// 这里也同样没有进行转换。类型形参作为输出,却声明为逆变(只用做输入)
public interface IInvalidCovariant<in T>{
T GetValue();
}