[.Net 4.0]泛型的协变,以及高阶函数对泛型的影响 Part 1
按:这文章算是上星期与装配脑袋一起讨论到的一些东西的总结。我试图用更多一点的代码把协变和反变解释得更浅显一点。大家也可以参考Ninputer同学的文章:
http://www.cnblogs.com/Ninputer/archive/2008/11/22/generic_covariant.html
为什么要有协变
首先来说明一下为什么会要协变。协变其实是一个相当简单的概念。我们知道在OO的语言中,可以把一个子类的实例赋值给一个基类的引用。就像这样:
Stream s = fs;
当引入了泛型之后,很容易我们会想到,像下面这样一个赋值是不是可以呢:
class ConcreteFileStream : ISample<FileStream>
{}
ISample<FileStream> iFs = new ConcreteFileStream();
ISample<Stream> iS = iFs; //这里的赋值是可以的吗?
直观上来看FileStream是Stream的子类,那么这个赋值当然应该是可行的。如果是可行的,那么我们称ISample<T>的泛型参数T,支持协变。
然而事实上,情况远没有这么简单。协变并不是像我们主观上认为的那样,总是可以的。在某些情况,泛型参数不能进行协变,但可以进行反变。所谓的反变,就是指对于这个泛型参数,我们可以做如下的赋值:
{}
ISample<Stream> iS = new ConcreteStream();
ISample<FileStream> iFs = iS; //如果这样的赋值可以进行,则称这个泛型参数支持反变
这样的赋值竟然是可以的?是不是有违我们的直觉?接下来我们讨论一下什么情况下可以协变,什么情况下可以反变,而什么情况下两者都不可以。
这里需要说明的一点是,我们这里说“一个赋值可以进行”,意思是指这个赋值不会引发类型不安全的形为。不会因此,导致类型相关的异常。在实际使用中,C#编译器会检测到“这样一个赋值是不可以进行的”,从而会引发问题的代码不能通过编译。而泛型参数也要显示的声明成是否可以协变与反变。
什么时候可以协变与反变?
之前提到了,要进行协变,远比我们的直觉要复杂的多。一个泛型参数是不是可以协变,取决于接口所定义的方法是如何使用这个泛型参数的。让我们来看下面的例子:
{
T foo();
}
此时,T是被用作返回值的类型。而在这种情况下,T是支持协变的。请看以下的代码:
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,说明它可以协变,像这样:
{}
接下来让我们看一个不能协变的例子:
{
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<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关键字来显示地说明这一点:
{
void foo(T t);
}
所以,协变与反变的一般定义如下:
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相关推荐
- 【Android入门】5、Broadcast 广播、Kotlin 的高阶函数、泛型、委托
六.BroadCast 广播 广播用于在Android系统内实现通知,概念较为简单 为了实现上述效果, 代码如下 基础类如下, 定义了receiver, 当收到消息时, 触发receiver逻辑(弹窗 ...
- python匿名函数调用_python3笔记十六:python匿名函数和高阶函数
一:学习内容 lambda函数 map函数与reduce函数 filter函数 sorted函数 二:匿名函数-lambda 1.概念:不使用def这样的语句去定义函数,使用lambda来创建匿名函数 ...
- Python之高阶函数(abs、map、reduce、filter、lambda匿名函数)
Python之高阶函数(abs.map.reduce.filter.lambda匿名函数) 什么是内置高阶函数 高阶函数:一个函数可以作为参数传给另外一个函数,或者一个函数的返回值为另外一个函数(若返 ...
- c2064 项不会计算为接受0个参数的函数_【JS必知必会】高阶函数详解与实战
本文涵盖 前言 高级函数概念 函数作为参数的高阶函数 map filter reduce sort详解与实战 函数作为返回值的高阶函数 isType函数与add求和函数 如何自己创建高阶函数 前言 一 ...
- [译]Effective Kotlin系列之探索高阶函数中inline修饰符(三)
简述: 不知道是否有小伙伴还记得我们之前的Effective Kotlin翻译系列,之前一直忙于赶时髦研究Kotlin 1.3中的新特性.把此系列耽搁了,赶完时髦了还是得踏实探究本质和基础,从今天开始 ...
- Kotlin中的高阶函数
博客地址sguotao.top/Kotlin-2018- 在Kotlin中,高阶函数是指将一个函数作为另一个函数的参数或者返回值.如果用f(x).g(x)用来表示两个函数,那么高阶函数可以表示为f(g ...
- functools 可调用对象上的高阶函数和操作
functools-可调用对象上的高阶函数和操作 functools 该模块用于高阶函数:作用于或返回其他函数的函数.通常,就此模块而言,任何可调用对象都可以视为函数. 该模块定义了以下功能:func ...
- Kotlin——高阶函数详解与标准的高阶函数使用
一.高阶函数介绍 在介绍高阶函数之前,或许您先应该了解Kotlin中,基础函数的使用与定义.您可以参见Kotlin--初级篇(七):函数(方法)基础使用这边文章的用法. 在Kotlin中,高阶函数即指 ...
- 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 ...
最新文章
- ajax请求数据渲染个人中心页面
- c语言动态链表creat函数,用create建立动态链表
- 解决PRINT函数UTF-8问题
- linux shell eval,【shell】bash shell 中 set 和 eval 命令的使用
- plugin工程及与flutter工程通信原理
- 51 nod 最长公共子序列问题(打印路径)
- 计算机始业课教案,始业课教案
- 2011年11月-2012年10月份 历时一年的 博客回顾
- HarmonyOS:Preferences的封装使用与避坑
- 前端导出word图片
- JAVA高级工程师笔试面试题
- ARM开发板如何安装Linux系统
- 基于ASP.NET通用后台管理系统模板
- 解决 java.lang.annotation.AnnotationFormatError: Invalid default: public abstract java.lang.Class org.
- 外文翻译之 Removing Camera Shake from a Single Photograph
- 在 VS Code 中使用 Git
- 让ie浏览器成为支持html5的浏览器的解决方法(使用html5shiv)
- 试图5天学会python——Mooc 实例
- 【Android】电源管理,进入和退出 Suspend To RAM
- Node.js开发的WeMall 6.0正式发布