C#中的协变和逆变

C#从2.0开始推出了泛型,极大的简化了相同算法对应不同数据类型的工作。在此基础上,C#4.0更进一步推出了泛型中的协变和逆变,有不少人对这个概念感觉比较模糊,今天就让我们来简单的捋一捋。

最常见的情况

考虑下面代码

object[] objs = new string[]{};
List<object> objList = new List<string>();

这段代码中,第一句没问题,虽然被Jon Skeet认为是性能低下并且需要避免使用的,但是好歹是合法的语法。第二句编译器会报错,会说无法隐式转换List<string>List<object>。按照相同的思路,下面这段代码似乎也应该报错。

IEumerable<object> strs = new List<string>();

但实际情况是,这是一段合法的代码。
嗯。。是不是感觉有点头晕,其实在背后,是协变和逆变在起作用。
 

协变

协变英文名为Covariance,在泛型方法的原型里面以out表示,有out关键字修饰的参数可以在声明需要父类参数的泛型的地方使用需要子类参数的泛型实例,比如我们刚刚看到的IEnumerable<T>

public interface IEnumerable<out T> : IEnumerable

所以在我们声明需要一个使用父类参数的IEnumerable的地方,我们可以提供一个使用子类参数的IEnumerable。

class Person{}

class Manager : Person{}

IEnumerable<Person> persons = new List<Manager>();
foreach(var person in persons){}

这段代码是完全合法的,我们在需要IEnumerable<Person>的地方提供了一个IEnumerable<Manager>。但是为什么这样是合法的呢,因为我们的IEnumerable主要作用是负责提供Person(只读),在这里是通过遍历的方式提供Person实例。Manager作为Person的子类,把它当做Person提供给外部是完全没有问题的。反过来,IEnumerable接口不允许通过本接口添加数据,这也是协变可以实现的一个前提。试想,如果IEnumberable<Person>允许通过接口添加数据到集合,但是本身其实例又是一个List<Manager>,那么会出现把Person添加到Manager里面的非法情况。
 

逆变

逆变英文名叫Contravariance,在泛型的方法签名中以in表示。和协变相反,逆变是指可以使用基于父类参数的泛型实例在要求子类参数的泛型的场合,我们还是来看个例子。

		class ComparePerson : IComparer<Person>
        {
            public int Compare(Person x, Person y)
            {
                throw new NotImplementedException();
            }
        }

		IComparer<Manager> comp = new ComparePerson();
        List<Manager> managers = new List<Manager>();
        managers.Sort(comp);

同样,这段代码也是合理的,在需要IComparer<Manager>的地方,我们实际传递了一个IComparer<Person>的实例。从语法上面看,是因为IComparer的声明中实现了逆变。

public interface IComparer<in T>

而从语义上看,也是合理的。因为这个IComparer的实例在具体使用中,他的作用是接受一个参数,返回一个int。所以他声明能接收Person参数却实际接受到了一个Manager实例(List里面装的是Manager),这是完全合法的。这可以看做是协变的反面——只写,只要不返回声明为逆变的类型参数的实例,就是安全的。
 

在自定义泛型中使用协变和逆变

刚刚看到的例子都是系统自带的泛型,那我们在自定义泛型的时候,能通过inout来指定协变和逆变吗?
答案是肯定的,通过指定这两个关键字,我们可以方便的指定协变和逆变,只是要注意,如果我们的泛型里面有协变类型的参数(可能破坏只读),或者允许返回逆变类型(可能破坏只写),那么编译器会报告错误。

delegate type2 MyGeneric<in type1, out type2>(type1 arg1); //这是没问题的
delegate type1 MyGeneric<in type1, out type2>(type1 arg1); //编译器报错,尝试返回逆变类型
delegate type2 MyGeneric<in type1, out type2>(type2 arg2); //编译器报错,在参数中使用协变类型

在具体使用中,

MyGeneric<Manager, Person> myGenericeInstance = (Person) => new Manager();

这样协变和逆变都可以在同一个泛型委托中使用了。在使用的时候,myGenericeInstance会被使用在一个提供了Manager实例为参数,并且期望返回一个Person实例的上下文中,这无疑是安全的。
 
总之一句话,在泛型中,如果函数类型参数是只读或者只写,那么就可以使用协变或者逆变。如果类型参数无法保证只读或只写,这种类型参数既不能协变也不能逆变,只能精确类型匹配。记住这个将是理解协变和逆变的关键。

  • 5
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值