C# 4.0新特性-"协变"与"逆变"以及背后的编程思想

在《上篇》中我们揭示了“缺省参数”的本质,现在我们接着来谈谈C#4.0中另一个重要的新特性:协变(Covariance)与逆变(Contravariance)。对于协变与逆变,大家肯定不会感到陌生,但是我相信有很多人不能很清晰地说出他们之间的区别。我希望通过这篇文章能够让读者更加深刻的认识协变与逆变。但是也不排除另一种可能,那就是读者这篇文章你对这两个概念更加模糊。文章一些内容仅代表个人观点,如有不妥,还望指正。

目录 一、两个概念:强类型与弱类型 二、委托中的协变与逆变的使用 三、接口中的协变与逆变的使用 四、从Func<T,TResult>看协变与逆变的本质 五、逆变实现了“算法”的重用

一、两个概念:强类型与弱类型

为了后面叙述方便,我现在这里自定义两个概念:强类型和弱类型。在本篇文章中,强类型和弱类型指的是两个具有直接或者间接继承关系的两个类。如果一个类是另一个类的直接或者间接基类,那么它为弱类型,直接或者间接子类为强类型。后续的介绍中会用到的两个类Foo和Bar先定义在这里。Bar继承自Foo。Foo是弱类型,而Bar则是强类型。

   1: public class Foo
   2: {
   3:     //Others Members...
   4: }
   5: public class Bar:Foo
   6: {
   7:     //Others Members...
   8: }

有了强类型和弱类型的概念,我们就可以这样的定义协变和逆变:如果类型TBar是基于强类型Bar的类型(比如类型参数为Bar的泛型类型,或者是参数/返回值类型为Bar的委托),而类型TFoo是基于弱类型Foo的类型,协变就是将TBar类型的实例赋值给TFoo类型的变量,而逆变则是将TFoo类型的实例赋值给TBar类型的变量。

二、委托中的协变与逆变的使用

协变和逆变主要体现在两个地方:接口和委托,先来看看在委托中如何使用协变和逆变。现在我们定义了如下一个表示无参函数的泛型委托Function<T>,类型参数为函数返回值的类型。泛型参数之前添加了一个out关键字表示T是一个协变变体。那么在使用过程中,基于强类型的委托Fucntion<Bar>实例就可以赋值给基于弱类型的委托Fucntion<Foo>变量。

   1: public delegate T Function<out T>();
   2: class Program
   3: {
   4:     static void Main()
   5:     {
   6:         Function<Bar> funcBar = new Function<Bar>(GetInstance);
   7:         Function<Foo> funcFoo = funcBar;
   8:         Foo foo = funcFoo();
   9:     }
  10:     static Bar GetInstance()
  11:     {
  12:         return new Bar();
  13:     }
  14: }

接下来介绍逆变委托的用法。下面定义了一个名称为Operate的泛型委托,接受一个具有泛型参数类型的参数。在定义泛型参数前添加了in关键字,表示T是一个基于逆变的变体。由于使用了逆变,我们就可以将基于弱类型的委托Operate<Foo>实例就可以赋值给基于强类型的委托Operate<Bar>变量。

   1: public delegate void Operate<in T>(T instance);
   2: class Program
   3: {
   4:     static void Main()
   5:     {
   6:         Operate<Foo> opFoo = new Operate<Foo>(DoSth);
   7:         Operate<Bar> opBar = opFoo;
   8:         opBar(new Bar());
   9:     }
  10:     static void DoSth(Foo foo)
  11:     {
  12:         //Others...
  13:     }
  14: }

三、接口中的协变与逆变的使用

接下来我们同样通过一个简单的例子来说明在接口中如何使用协变和逆变。下面定义了一个继承自 IEnumerable<T>接口的IGroup<out T>集合类型,和上面一样,泛型参数T之前的out关键字表明这是一个协变。既然是协变,我们就可以将一个基于强类型的委托IGroup<Bar>实例就可以赋值给基于弱类型的委托IGroup<Foo>变量。

   1: public interface IGroup<out T> : IEnumerable<T>
   2: { }
   3: public class Group<T> : List<T>, IGroup<T>
   4: { }
   5: public delegate void Operate<in T>(T instance);
   6: class Program
   7: {
   8:     static void Main()
   9:     {
  10:         IGroup<Bar> groupOfBar = new Group<Bar>();
  11:         IGroup<Foo> groupOfFoo = groupOfBar;
  12:         //Others...
  13:     }
  14: }

下面是一个逆变接口的例子。首先定义了一个IPaintable的接口,里面定义了一个可读写的Color属性,便是实现该接口的类型的对象具有自己的颜色,并可以改变颜色。类型Car实现了该接口。接口IBrush<in T>定义了一把刷子,泛型类型需要实现IPaintable接口,in关键字表明这是一个逆变。方法Paint用于将指定的对象粉刷成相应的颜色,表示被粉刷的对象的类型为泛型参数类型。Brush<T>实现了该接口。由于IBrush<in T>定义成逆变,我们就可以将基于弱类型的委托IBrush<IPaintable>实例就可以赋值给基于强类型的委托IBrush<Car>变量。

   1: public interface IPaintable
   2: {
   3:     Color Color { get; set; }
   4: }
   5: public class Car : IPaintable
   6: {
   7:     public Color Color { get; set; }
   8: }
   9:  
  10: public interface IBrush<in T> where T : IPaintable
  11: {
  12:     void Paint(T objectToPaint, Color color);
  13: }
  14: public class Brush<T> : IBrush<T> where T : IPaintable
  15: {
  16:     public void Paint(T objectToPaint, Color color)
  17:     {
  18:         objectToPaint.Color = color;
  19:     }
  20: }
  21:  
  22: class Program
  23: {
  24:     static void Main()
  25:     {
  26:         IBrush<IPaintable> brush = new Brush<IPaintable>();
  27:         IBrush<Car> carBrush = brush;
  28:         Car car = new Car();
  29:         carBrush.Paint(car, Color.Red);
  30:         Console.WriteLine(car.Color.Name);
  31:     }
  32: }

四、从Func<T,TResult>看协变与逆变的本质

接下来我们来谈谈协变和逆变的本质区别是什么。在这里我们以我们非常熟悉的一个委托Func<T, TResult>作为例子,下面给出了该委托的定义。我们可以看到Func<T, TResult>定义的两个泛型参数分别属于逆变和协变。具体来说输入参数类型为逆变,返回值类型为协变。

   1: public delegate TResult Func<in T, out TResult>(T arg);

再重申以下这句话“输入参数类型为逆变,返回值类型为协变”。然后,你再想想为什么逆变用in关键字,而协变用out关键字。这两个不是偶然,实际上我们可以将协变/逆变与输出/输入匹配起来。

我们再从另一个角度来理解协变与逆变。我们知道接口代表一种契约,当一个类型实现一个接口的时候就相当于签署了这份契约,所以必须是实现接口中所有的成员。实际上类型继承也属于一种契约关系,基类定义契约,子类“签署”该契约。对于类型系统来说,接口实现和类型继承本质上是一致的。契约是弱类型,签署这份契约的是强类型。

将契约的观点应用在委托上面,委托实际上定义了一个方法的签名(参数列表和返回值),那么参数和返回值的类型就是契约,现在的关键是谁去履行这份契约。所有参数是外界传入的,所以基于参数的契约履行者来源于外部,也就是被赋值变量的类型,所以被赋值变量类型是强类型。而对于代理本身来说,参数是一种输入,也就是一种采用in关键字表示的逆变。

而对于委托的返回值,这是给外部服务的,是委托自身对外界的一种承诺,所以它自己是契约的履行着,因此它自己应该是强类型。相应地,对于代理本身来说,返回值是一种输出,也就是一种采用out关键字定义的协变。

也正式因为这个原因,对于一个委托,你不能将参数类型定义成成协变,也不能将返回类型定义成逆变。下面两中变体定义方式都是不能通过编译的。

   1: delegate TResult Fucntion<out T, TResult>(T arg);
   2: delegate TResult Fucntion<T, in TResult>(T arg);

说到这里,我想有人要问一个问题,既然输入表示逆变,输出表示协变,委托的输出参数应该定义成协变了?非也,实际上输出参数在这里既输出输出,也输出输入(毕竟调用的时候需要指定一个对应类型的对象)。也正是为此,输出参数的类型及不能定义成协变,也不能定义成逆变。所以下面两种变体的定义也是不能通过编译的。

   1: delegate void Action<in T>(out T arg);
   2: delegate void Action<out T>(out T arg);

虽然这里指介绍了关于委托的协变与逆变,上面提到的契约和输入/输出的关系也同样适用于基于接口的协变与逆变。你自己可以采用这样的方式去分析上面一部分我们定义的IGroup<Foo>和IBrush<in T>。

五、逆变实现了“算法”的重用

实际上关系协变和逆变体现出来的编程思想,还有一种我比较推崇的说法,那就是:协变是继承的体现,而逆变体现的则是多态(可以参考idior的文章《Covariance and Contravariance》)。实际上这与上面分析的契约关系本质上是一致的。

关于逆变,在这里请容我再啰嗦一句:逆变背后蕴藏的编程思想体现出了对算法的重用——我们为基类定义了一套操作,可以自动应用于所有子类的对象。

刚刚看了园友施凡的文章,写得很好,有兴趣的可以读读《.NET 4.0中的泛型协变和反变》。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Coding迪斯尼

使用普拉特分析法解析极为复杂的算术表达式

12430
来自专栏Pythonista

Golang之匿名函数和闭包

 基本概念 闭包是可以包含自由(未绑定到特定对象)变量的代码块,这些变量不在这个代码块内或者 任何全局上下文中定义,而是在定义代码块的环境中定义。要执行的代码块...

32910
来自专栏函数式编程语言及工具

Scalaz(18)- Monad: ReaderWriterState-可以是一种简单的编程语言

  说道FP,我们马上会联想到Monad。我们说过Monad的代表函数flatMap可以把两个运算F[A],F[B]连续起来,这样就可以从程序的意义上形成一种串...

20570
来自专栏静晴轩

JavaScript对象length

前几日有在Javascript数组操作一文中稍提及了数组的length属性;深入一点探究,就发现JS这length确有许多难为所知的特性。这就边学边探究下这朵奇...

38580
来自专栏黑泽君的专栏

java基础学习_IO流02_递归、IO流字节流、IO流字符流(自学)_day20总结

java基础学习_IO流02_递归、IO流字节流、IO流字符流(自学)_day20总结

7010
来自专栏猿人谷

一个正则表达式测试(只可输入中文、字母和数字)

  在项目中碰到了正则表达式的运用,正则还是非常强大的,不管什么编程语言,基本上都可以用到。之前在用java时特别是对用户名或密码使用正则非常爽,写脚本上用正则...

83860
来自专栏醒者呆

大师的小玩具——泛型精解

掌握Java的泛型,这一篇文章足够了。 关键字:泛型,Iterable接口,斐波那契数列,匿名内部类,枚举,反射,可变参数列表,Set 一般类和方...

44650
来自专栏C#

C#中的委托解析

    谈及到C#的基本特性,“委托”是不得不去了解和深入分析的一个特性。对于大多数刚入门的程序员谈到“委托”时,都会想到“将方法作为方法的参数进行传递”,很多...

21990
来自专栏EAWorld

了解、接受和利用Java中的Optional (类)

作者:EUGEN PARASCHIV 译者:海松 原题: Understanding, Accepting and Leveraging Optional in...

36960
来自专栏从流域到海域

Python基本数据类型

其实之前有一篇博客:C\C#\Java\Python 基本数据类型比较 https://cloud.tencent.com/developer/article...

25560

扫码关注云+社区

领取腾讯云代金券