C#4.0新特性(3):变性 Variance(逆变与协变)

 一句话总结:协变让一个粗粒度接口(或委托)可以接收一个更加具体的接口(或委托)作为参数(或返回值);逆变让一个接口(或委托)的参数类型(或返回值)类型更加具体化,也就是参数类型更强,更明确。

  通常,协变类型参数可用作委托的返回类型,而逆变类型参数可用作参数类型。对于接口,协变类型参数可用作接口的方法的返回类型,而逆变类型参数可用作接口的方法的参数类型。

协变

我们先来看下面一个来自MSDN的例子:
01 // 协变

02 IEnumerable<string> strings = new List<string>();

03 IEnumerable<object> objects = strings;

04 //大家看到了么一个声明为IEnumerable<string>的接口类型被赋给了一个更低级别的IEnumerable<object>.

05 //对,这就是协变。再来看一个例子:

06 class Base

07 {

08     public static void PrintBases(IEnumerable<Base> bases)

09     {

10         foreach(Base b in bases)

11         {

12             Console.WriteLine(b);

13         }

14

15     }

16 }

17

18 class Derived : Base

19 {

20     public static void Main()

21     {

22         List<Derived> dlist = new List<Derived>();

23         Derived.PrintBases(dlist);

24      //由于IEnumerable<T>接口是协变的,所以PrintBases(IEnumerable<Base> bases)

25         //可以接收一个更加具体化的IEnumerable<Derived>作为其参数。

26         IEnumerable<Base> bIEnum = dlist;

27     }

28 }
下面给协变下个定义:

  协变:让一个带有协变参数的泛型接口(或委托)可以接收类型更加精细化,具体化的泛型接口(或委托)作为参数,可以看成OO中多态的一个延伸。

逆变

1 // 逆变

2 // Assume that the following method is in the class:

3 // static void SetObject(object o) { }

4 Action<object> actObject = SetObject;

5 Action<string> actString = actObject;

6 //委托actString中以后要使用更加精细化的类型string不能再使用object啦!

7 string strHello(“Hello”);

8 actString(strHello);
大家看到了么?一个声明为Action<object>的类型被赋给了一个Action<string>,大家都知道,Action<T>接收参数,没有返回值,所以其中的object和string是其参数,这个过程其实就是参数的约束更加强了,也就是说让参数类型更加精细化。下面我们来给逆变下个定义:

  逆变:让一个带有协变参数的泛型接口(或委托)可以接收粒度更粗的泛型接口或委托作为参数,这个过程实际上是参数类型更加精细化的过程。

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

为了后面叙述方便,我现在这里自定义两个概念:强类型和弱类型。在本篇文章中,强类型和弱类型指的是两个具有直接或者间接继承关系的两个类。如果一个类是另一个类的直接或者间接基类,那么它为弱类型,直接或者间接子类为强类型。后续的介绍中会用到的两个类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,类型参数为函数返回值的类型。泛型参数之前添加了一个out关键字表示T是一个协变变体。那么在使用过程中,基于强类型的委托Fucntion实例就可以赋值给基于弱类型的委托Fucntion变量。

01 public delegate T Function<out T>();

02  class Program

03  {

04      static void Main()

05     {

06         Function funcBar = new Function(GetInstance);

07         Function funcFoo = funcBar;

08         Foo foo = funcFoo();

09     }

10      static Bar GetInstance()

11      {

12          return new Bar();

13      }

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

01 public delegate void Operate<in T>(T instance);

02 class Program

03 {

04 static void Main()

05 {

06     Operate opFoo = new Operate(DoSth);

07     Operate opBar = opFoo;

08     opBar(new Bar());

09 }

10 static void DoSth(Foo foo)

11 {

12     //Others...

13 }

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

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

01 public interface IGroup<out T> : IEnumerable

02 { }

03

04 public class Group : List, IGroup

05 { }

06

07 public delegate void Operate<in T>(T instance);

08

09 class Program

10 {

11     static void Main()

12     {

13         IGroup groupOfBar = new Group();

14         IGroup groupOfFoo = groupOfBar;

15         //Others...

16     }

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

public interface IPaintable  

    Color Color { get; set; }  

public class Car : IPaintable  
{  
    public Color Color { get; set; }  
}

public interface IBrush<in T> where T : IPaintable  
{  
    void Paint(T objectToPaint, Color color);  
}  
public class Brush : IBrush where T : IPaintable  
{  
    public void Paint(T objectToPaint, Color color)  
    {  
        objectToPaint.Color = color;  
    }  
}

class Program  
{  
    static void Main()  
    {  
        IBrush brush = new Brush();  
        IBrush carBrush = brush;  
       Car car = new Car();  
       carBrush.Paint(car, Color.Red);  
       Console.WriteLine(car.Color.Name);  
    }  
}四、从Func看协变与逆变的本质

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

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

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

2 delegate void Action<out T>(out T arg);
五、逆变实现了“算法”的重用

实际上关系协变和逆变体现出来的编程思想,还有一种我比较推崇的说法,那就是:协变是继承的体现,而逆变体现的则是多态。实际上这与上面分析的契约关系本质上是一致的。

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

完整示例

01 /// <summary>

02 /// 协变和逆变允许数组类型、委托类型和泛型类型参数进行隐式引用转换。 协变保留分配兼容性,逆变与之相反。

03 /// 协变和逆变只能用于引用类型,不能用于值类型或void

04 /// </summary>

05 public class CovarianceAndContravariance : IFace

06 {

07     public CovarianceAndContravariance()

08     {

09         ///分配兼容性

10         string str = "test";

11         object obj = str;

12

13         ///数组的协变允许派生程度更大的类型的数组隐式转换为派生程度更小的类型的数组,但是此操作运行时不是类型安全的操作.

14         object[] array = new String[10];

15         // array[0] = 10;

16

17         ///方法的协变和逆变

18         Func<object> del = GetString;

19         //Func<string> del00 = GetObject;   //返回值不能逆变

20         Action<string> del2 = SetObject;

21         //Action<object> del22 = SetString; //参数不能协变

22         Action<object> actObject = SetObject;

23         Action<string> actString = actObject;

24

25         // 泛型类型参数进行隐式引用转换

26         IEnumerable<string> strings = new List<string>();

27         IEnumerable<object> objects = strings;

28     }

29

30     static object GetObject() { return null; }

31     static void SetObject(object obj) { }

32

33     static string GetString() { return ""; }

34     static void SetString(string str) { }

35

36     /// <summary>

37     /// 接口不存在协变和逆变

38     /// </summary>

39     /// <param name="obj"></param>

40     /// <returns></returns>

41     public string func(object obj)

42     {

43         return null;

44     }

45     public object func2(string obj)

46     {

47         return null;

48     }

49 }

50 public interface IFace

51 {

52     string func(object obj);

53     object func2(string obj);

54 }
  一句话总结:协变让一个粗粒度接口(或委托)可以接收一个更加具体的接口(或委托)作为参数(或返回值);逆变让一个接口(或委托)的参数类型(或返回值)类型更加具体化,也就是参数类型更强,更明确。

  通常,协变类型参数可用作委托的返回类型,而逆变类型参数可用作参数类型。对于接口,协变类型参数可用作接口的方法的返回类型,而逆变类型参数可用作接口的方法的参数类型。

时间: 2024-10-07 13:03:05

C#4.0新特性(3):变性 Variance(逆变与协变)的相关文章

Day07 jdk5.0新特性&Junit&反射

day07总结 今日内容 MyEclipse安装与使用 JUnit使用 泛型 1.5新特性 自动装箱拆箱 增强for 静态导入 可变参数方法 枚举 反射 MyEclipse安装与使用(yes) 安装MyEclipse 先安装了JDK ? MyEclipse介绍 ? MyEclipse是Eclipse的一个插件: MyEclipse是需要花钱的: MyEclipse官网不在欢迎中国人登录: ? MyEclipse使用 ? 1 创建项目 选择工作空间: 工作空间路径不能有空格和中文: 工作空间以班名

Atitit.&#160;C#.net&#160;clr&#160;2.0&#160;&#160;4.0新特性

Atitit. C#.net clr 2.0  4.0新特性 1. CLR内部结构1 2. CLR 版本发展史3 3. CLR 2.0 3 4. CLR 4 新特性 概览4 4.1.1.  托管与本地代码的互操作5 4.1.2.    垃圾回收6 4.1.3.    代码约定6 4.1.4.    Corrupted state exception6 4.1.5.     新的安全模型7 4.1.6.     同一个进程,多个CLR7 4.1.7.     基本类库7 5. CLR最新发展8 6

day07 MyEclipse 安装 jdk5.0 新特性

1.myeclipse的安装和使用 * eclipse:是一个免费的开发工具    * myeclipse:是一个收费的插件,破解myeclipse,        ** 安装目录的要求: 不能有中文和空格        ** 安装完成之后,选择一个工作空间 ,这个工作空间不能有中文和空格    * 破解myeclipse        ** 运行run.bat文件,但是运行之前,必须要安装jdk,通过配置环境变量 * myeclipse的使用        * 创建一个工程          

AFNetworking 2.0 新特性讲解之AFHTTPSessionManager

AFNetworking 2.0 新特性讲解之AFHTTPSessionManager (2014-02-17 11:56:24) 转载▼     AFNetworking 2.0 相比1.0 API 接口改动还是很大的. 其中一个便是 AFURLSessionManager,当然如果你不太熟悉,或者为了兼容低版本,你依然可以选择AFHTTPRequestOperationManager,AFURLSessionManager是基于 NSURLSessionConfiguration(IOS 7

Servlet 3.0 新特性详解

转自:https://www.ibm.com/developerworks/cn/java/j-lo-servlet30/ Servlet 3.0 新特性详解 张 建平2010 年 4 月 23 日发布 WeiboGoogle+用电子邮件发送本页面 6 Servlet 3.0 新特性概述 Servlet 3.0 作为 Java EE 6 规范体系中一员,随着 Java EE 6 规范一起发布.该版本在前一版本(Servlet 2.5)的基础上提供了若干新特性用于简化 Web 应用的开发和部署.其

android 7.0 新特性 和对开发者的影响

android 7.0新特性 - jiabailong的专栏 - 博客频道 - CSDN.NEThttp://blog.csdn.net/jiabailong/article/details/52411300 android 7.0对开发者会有哪些影响 - jiabailong的专栏 - 博客频道 - CSDN.NEThttp://blog.csdn.net/jiabailong/article/details/52411353 android 7.0 多窗口及新特性demo - jiabail

C#6.0新特性

C#6.0新特性怎么用 系列文章 Visual Studio 2015速递(1)——C#6.0新特性怎么用 前文提到过一个神器叫Resharper,功能强大,编码效率和代码质量那是蹭蹭的涨,但是神器的最大问题是太耗费资源了,每次系统提示内存不足的时候,那叫一个纠结啊.因此每次新的VS发布的时候都情不自禁的查看是否增强编辑功能,情不自禁的讨论一番,这次VS2015也不例外. 去年微软放出Roslyn的时候,微软就曾经放出过一个“尝鲜”版的VS编辑增强功能,恰恰就是重构(reflector),话说这

ASP.NET MVC—1、前期知识储备(C#3.0新特性)

在学习ASP.NET MVC之前,有必要先了解一下C#3.0所带来的新的语法特性,这一点尤为重要,因为在MVC项目中我们利用C#3.0的新特性将会大大的提高我们的开发效率,同时,在MVC项目中你将到处可以看到C#3.0新特性的身影. C#3.0新特性 自动属性 隐式类型 var 对象初始化器与集合初始化器 匿名类 扩展方法 Lambda表达式 自动属性 这个概念很简单,其简化了我们在.NET的时候手写一堆私有成员+属性的编程方式,我们只需要使用如下方式声明一个属性,编译器会自动生成所需的成员变量

Spark1.0.0新特性

Spark1.0.0 release于2014-05-30日正式公布,标志Spark正式进入1.X的时代.Spark1.0.0带来了各种新的特性,并提供了更好的API支持:Spark1.0.0添加了Spark SQL这一个新的重要组件,用于载入和操作Spark的结构化数据:Spark1.0.0增强了现有的标准库(ML,streaming,GraphX),同一时候还增强了Java和Python语言的支持:最后,Spark1.0.0在运维上做了非常大的改进,包含支持Hadoop/YARN安全机制.使