C#泛型的抗变与协变
学习自
C#本质论6.0
https://www.cnblogs.com/pugang/archive/2011/11/09/2242380.html
Overview
一直以来,被抗变与协变的定义搞得头昏脑涨掰持不清,如果在加上泛型在其中作祟,就更加两眼发懵了。所以就暂时略过了这一拦路虎,但是今天在学习Kotlin泛型的时候再一次碰到了这里拦路虎,只硬着头皮迎难而上了。
抗边与协变的定义
协变: 子类想父类方向的类型转换称之为协变
//string[] 向 object[] 协变转换
string [] strArray = new string[10];
object [] objArray = strArray;
抗变: 父类向子类方向的类型转换称之为抗变
//委托的抗变
Action<object> actObj = Set;
Action<string> actStr = actObj;
抗变和协变是针对于泛型接口和委托来说的,但是数组也是存在协变的,但是数组的协变是不安全的
泛型的抗变与协变
受限制的泛型协变
在这里有一个让初学泛型的人非常疑惑的一个问题:
** 为什么List 的集合不能复制为List 的集合 **
虽然string可以向object协变转化, 但是因为类型会不安全,这种操作是没法的。
唯恐说不明白,举个例子吧
咱们 假定,下面的代码不会报错,且看我一步步分析
List<string> strList = new List<string>();
//在这里会报错,但是咱们先假设他们可以顺利执行
List<object> objList = strList;
//因为objList实际其中装着的是strList,那么可以肯定将string类型的数据添加到集合中是完全没有问题的
objList.Add("Hello");
//因为objList实际其中装着的是strList,因为string类型和int类型肯定是会有冲突的,假设这样的代码可以执行那么只有崩溃一条路可以走
objList.Add(1);
//这就是为什么,List<string> 不能赋值给List<int>
out 修饰符允许协变性
之所以会出现上面的问题,是因为List 不仅仅可以读取,还可以被写入,这样就造成了类型不安全。所以上述操作是不允许的。因为问题出在了写入上面,所以如果我们提供一个只读的操作是否就可以避免这种错误呢?
看下列的代码
public interface IReadOnlyPair<T>
{
T First { get; }
T Second { get; }
}
public interface IPair<T>
{
T First { get; set; }
T Second { get; set; }
}
public class Pair<T> : IPair<T>, IReadOnlyPair<T>
{
public Pair(T first, T second)
{
}
public T First
{
get
{
throw new System.NotImplementedException();
}
set
{
throw new System.NotImplementedException();
}
}
public T Second
{
get
{
throw new System.NotImplementedException();
}
set
{
throw new System.NotImplementedException();
}
}
}
static void Main(string[] args)
{
Pair<string> strPair = new Pair<string>("First", "Second");
//虽然代码看上去十分合理,但是在这里仍然会报错,因为类型仍然存在不安全
IReadOnlyPair<object> objPair = strPair;
Console.WriteLine(objPair.First);
Console.WriteLine(objPair.Second);
}
IReaderOnlyPair是只读的,所以它根本就没有写入操作,那么这样就避免了上面所提到的因为添加操作而出现的类型安全问题,所以会编译应该不会有问题,那么咱们现在的这个是否能够通过呢?
答案是否定的,因为类型仍然不安全,为了结果这种问题C#4.0提供了out修饰符来允许我们进行类型安全的协变
为什么是不安全的看下面代码就知道了
public interface IReadOnlyPair<T>
{
T First { get; }
T Second { get; }
//如果接口中,又多出了下面的方法,那么泛型参数T就又成了了“输入”类型的泛型参数,在一次出现了类型不安全问题
void Set(T t);
}
为了支持协变,我们需要对代码进行一下小小的修改
public interface IReadOnlyPair<out T>
{
T First { get; }
T Second { get; }
}
out
修饰符的作用是,告诉编译器,这个泛型参数只用于 输出
, 即只用 返回值
和属性
, 经过这些变化之后,上面的代码就不会报错了。
但是协变转换,仍然是有一些限制的
- 只有泛型委托和泛型接口是可以进行协变转换的,而泛型类是不可以的
- 参与协变的泛型类型必须是引用类型,而不能是像
int
这样的值类型 - 接口或者委托声明为支持协变了的话,那么泛型参数只能用作
属性
和返回值
而不能用于方法的参数
泛型的抗变
抗变相对于协变是相对比较难一点的,但是只要抓住了其中的关键原因,理解起来也非常容易。
直接上例子比讲定义更清晰
static void Main(string[] args)
{
Action<object> actObj = Set;
//在这里你可能会感到奇怪,Object类型怎么可能可以直接地转换为string类型呢
Action<string> actStr = actObj;
actStr.Invoke("dfdf");
}
static void Set(object obj)
{
Console.WriteLine(obj.ToString());
}
在泛型接口和委托的协变中 遵循了这么一个原则 子类可以安全地调用父类的任何成员 ,上述代码中 actObj 委托指向了Set方法,接下来将actObj赋值给了 Action actStr, 那么现在actStr中存放的实际上是actObj对象指向的方法是 Set(object ojb) 方法,注意string类型可以安全地调用Object类中的所有的成员,所以actStr.Invoke("dfdf");
这句代码完全没有问题。
in类型修饰符,允许抗变
Action委托是支持抗变的,如果想让自己定义的泛型具有抗变的特性,需要在泛型类型前加上 in
关键字才行。
public delegate void Action<in T>(T obj);
抗变也有着一些规则限制
- 只能是引用类型
- 只能作为方法参数,不能用作返回值和属性
- 只有泛型委托和泛型接口是可以进行抗变转换,而泛型类是不可以的
结语
本文记录了我对泛型的抗变和协变的理解,以供大家参考,如果文中有什么谬误或不足之处欢迎指出。