按:这文章算是上星期与装配脑袋一起讨论到的一些东西的总结。我试图用更多一点的代码把协变和反变解释得更浅显一点。大家也可以参考Ninputer同学的文章:

http://www.cnblogs.com/Ninputer/archive/2008/11/22/generic_covariant.html

为什么要有协变

首先来说明一下为什么会要协变。协变其实是一个相当简单的概念。我们知道在OO的语言中,可以把一个子类的实例赋值给一个基类的引用。就像这样:

FileStream fs = new FileStream();
Stream s = fs;

当引入了泛型之后,很容易我们会想到,像下面这样一个赋值是不是可以呢:

interface ISample<T> {}
class ConcreteFileStream : ISample<FileStream>
{}

ISample<FileStream> iFs = new ConcreteFileStream();
ISample<Stream> iS = iFs; //这里的赋值是可以的吗?

直观上来看FileStream是Stream的子类,那么这个赋值当然应该是可行的。如果是可行的,那么我们称ISample<T>的泛型参数T,支持协变。

然而事实上,情况远没有这么简单。协变并不是像我们主观上认为的那样,总是可以的。在某些情况,泛型参数不能进行协变,但可以进行反变。所谓的反变,就是指对于这个泛型参数,我们可以做如下的赋值:

class ConcreteStream : ISample<Stream>
{}

ISample<Stream> iS = new ConcreteStream();
ISample<FileStream> iFs = iS; //如果这样的赋值可以进行,则称这个泛型参数支持反变

这样的赋值竟然是可以的?是不是有违我们的直觉?接下来我们讨论一下什么情况下可以协变,什么情况下可以反变,而什么情况下两者都不可以。

这里需要说明的一点是,我们这里说“一个赋值可以进行”,意思是指这个赋值不会引发类型不安全的形为。不会因此,导致类型相关的异常。在实际使用中,C#编译器会检测到“这样一个赋值是不可以进行的”,从而会引发问题的代码不能通过编译。而泛型参数也要显示的声明成是否可以协变与反变。

什么时候可以协变与反变?

之前提到了,要进行协变,远比我们的直觉要复杂的多。一个泛型参数是不是可以协变,取决于接口所定义的方法是如何使用这个泛型参数的。让我们来看下面的例子:

interface ISample<T>
{
    T foo();
}

此时,T是被用作返回值的类型。而在这种情况下,T是支持协变的。请看以下的代码:

ISample<FileStream> iFs = new ConcreteFileStream();

ISample<Stream> iS = iFs;
Stream s = iS.foo(); //iS.foo()返回FileStream对象,可以隐式转化为Stream类型,没有问题!!

当我们使用ISample<Stream>的时候,因为类型参数为Stream,所以代码中我们指望foo方法会返回一个Stream对象。而当iS实际指向一个ISample<FileStream>的对象时,foo方法会返回一个FileStream对象。而因为FileStream是Stream的子类,因此也是一个Stream对象。所以,在这里这个赋值不会引发任何问题。

在C# 4.0当中,如果一个泛型参数可以进行协变,我们要显示地进行声明,最通常的,当T被传出的时候,可以进行协变(这在有高阶函数的时候不成立,但我们稍后再讨论),所以我们要用一个out关键字来修饰T,说明它可以协变,像这样:

interface ISample<out T>
{}

接下来让我们看一个不能协变的例子:

interface ISample<T>
{
    void foo(T t);
}

ISample<FileStream> iFs = new ConcreteFileStream();

ISample<Stream> iS = iFs; //如果可以,会引发以下的情况,导致类型不安全

Stream s = new Stream();
iS.foo(s); //不能把s转化为FileStream!!!

我们可以看到当使用iS的时候,我们认为类型参数是Stream,因此调用foo的时候,我们可能会把一个Stream类型的对象当作参数传递给foo。而当iS实际指向一个ISample<FileStream>的时候,foo函数要求的是一个FileStream对象。而Stream是不能转化为FileStream的。所以如果允许这样的赋值,在运行时会有一个InvalidCastException。实际情况是,C#编译器会检测到这段代码的问题,不会让代码通过编译。而在这种情况下,iS不可以指向ISample<FileStream>,也就是说T不支持协变。比较有趣的是,在这种情况下,T可以进行反变:

ISample<Stream> iS = new ConcreteStream();

ISample<FileStream> iFs = iS;
FileStream fs = new FileStream();
iFs.foo(fs);//可以将fs转化为Stream类型,所以不会有类型不安全!

在这里,使用iFs的时候,因为T的类型被指定为FileStream,foo的名义签名为 void foo(FileStream t),所以我们有可能将一个FileStream类型的对象传递给foo函数。而当iFs实际指向一个ISample<Stream>时,foo的实际签名为void foo(Stream t),而当我们把fs传递给foo的时候,会发生一个从FileStream到Stream的转换。FileStream是子类,所以这个转换完全可行。在这里,我们把一个ISample<Stream>赋值给一个ISample<FileStream>,而不会引发类型不安全。这种情况,我们称T支持反变。我们要用in关键字来显示地说明这一点:

interface ISample<in T>
{
    void foo(T t);
}

所以,协变与反变的一般定义如下:

class Base {}
class Derived : Base {}
interface ISample<T> {}

ISample<Base> iB;
ISample<Derived> iD;

iB = iD; //如果这个赋值是类型安全的,那么T可以协变
iD = IB; //如果这个赋值是类型安全的,那么T可以反变

写到这里,你大概会觉得,当T用在参数的时候,可以反变,用作返回类型的时候,可以协变。当又是参数又是返回类型的时候,就既不能协变也不能反变了。在通常的应用中,这是正确的。这里所谓的通常,是指没有高阶函数存在的情况。很不幸的是,其实我们还蛮容易就会碰到有高阶函数的情况。那么具体的内容,让我们在下一篇里再继续。

转载于:https://www.cnblogs.com/Hush/archive/2008/11/22/1339140.html

[.Net 4.0]泛型的协变,以及高阶函数对泛型的影响 Part 1相关推荐

  1. 【Android入门】5、Broadcast 广播、Kotlin 的高阶函数、泛型、委托

    六.BroadCast 广播 广播用于在Android系统内实现通知,概念较为简单 为了实现上述效果, 代码如下 基础类如下, 定义了receiver, 当收到消息时, 触发receiver逻辑(弹窗 ...

  2. python匿名函数调用_python3笔记十六:python匿名函数和高阶函数

    一:学习内容 lambda函数 map函数与reduce函数 filter函数 sorted函数 二:匿名函数-lambda 1.概念:不使用def这样的语句去定义函数,使用lambda来创建匿名函数 ...

  3. Python之高阶函数(abs、map、reduce、filter、lambda匿名函数)

    Python之高阶函数(abs.map.reduce.filter.lambda匿名函数) 什么是内置高阶函数 高阶函数:一个函数可以作为参数传给另外一个函数,或者一个函数的返回值为另外一个函数(若返 ...

  4. c2064 项不会计算为接受0个参数的函数_【JS必知必会】高阶函数详解与实战

    本文涵盖 前言 高级函数概念 函数作为参数的高阶函数 map filter reduce sort详解与实战 函数作为返回值的高阶函数 isType函数与add求和函数 如何自己创建高阶函数 前言 一 ...

  5. [译]Effective Kotlin系列之探索高阶函数中inline修饰符(三)

    简述: 不知道是否有小伙伴还记得我们之前的Effective Kotlin翻译系列,之前一直忙于赶时髦研究Kotlin 1.3中的新特性.把此系列耽搁了,赶完时髦了还是得踏实探究本质和基础,从今天开始 ...

  6. Kotlin中的高阶函数

    博客地址sguotao.top/Kotlin-2018- 在Kotlin中,高阶函数是指将一个函数作为另一个函数的参数或者返回值.如果用f(x).g(x)用来表示两个函数,那么高阶函数可以表示为f(g ...

  7. functools 可调用对象上的高阶函数和操作

    functools-可调用对象上的高阶函数和操作 functools 该模块用于高阶函数:作用于或返回其他函数的函数.通常,就此模块而言,任何可调用对象都可以视为函数. 该模块定义了以下功能:func ...

  8. Kotlin——高阶函数详解与标准的高阶函数使用

    一.高阶函数介绍 在介绍高阶函数之前,或许您先应该了解Kotlin中,基础函数的使用与定义.您可以参见Kotlin--初级篇(七):函数(方法)基础使用这边文章的用法. 在Kotlin中,高阶函数即指 ...

  9. Kotlin系列四:标准函数、扩展函数、高阶函数、内联函数

    目录 一 标准函数 1.1 作用域函数 1.1.1 let 1.1.2  with 1.1.3 run 1.1.4 apply 1.1.5 also 1.1.6 takeIf 与 takeUnless ...

最新文章

  1. ajax请求数据渲染个人中心页面
  2. c语言动态链表creat函数,用create建立动态链表
  3. 解决PRINT函数UTF-8问题
  4. linux shell eval,【shell】bash shell 中 set 和 eval 命令的使用
  5. plugin工程及与flutter工程通信原理
  6. 51 nod 最长公共子序列问题(打印路径)
  7. 计算机始业课教案,始业课教案
  8. 2011年11月-2012年10月份 历时一年的 博客回顾
  9. HarmonyOS:Preferences的封装使用与避坑
  10. 前端导出word图片
  11. JAVA高级工程师笔试面试题
  12. ARM开发板如何安装Linux系统
  13. 基于ASP.NET通用后台管理系统模板
  14. 解决 java.lang.annotation.AnnotationFormatError: Invalid default: public abstract java.lang.Class org.
  15. 外文翻译之 Removing Camera Shake from a Single Photograph
  16. 在 VS Code 中使用 Git
  17. 让ie浏览器成为支持html5的浏览器的解决方法(使用html5shiv)
  18. 试图5天学会python——Mooc 实例
  19. 【Android】电源管理,进入和退出 Suspend To RAM
  20. Node.js开发的WeMall 6.0正式发布

热门文章

  1. 不设置DIV的宽高,让它相对于页面水平垂直居中
  2. 使用Maven自动部署Java Web项目到Tomcat问题小记
  3. paping使用来测试联通网站由于tcp协议导致的无法通信问题超时问题
  4. 链表的简单操作-----删除,添加,查找(Xcode)
  5. Oracle并行事务回滚相关参数及视图
  6. pt-table-checksum使用实践
  7. AspxTreeList获取选中项的值
  8. Webydo:一款在线自由创建网站的 Web 应用
  9. SQL SERVER2008 存储过程、表、视图、函数的权限
  10. 出差费用管理模块的几个问题