C#编程(二十九)----------泛型接口
泛型接口
定义
先来看一个简单的例子:
public class Sharp
{}
public class Rectangle:Sharp
{}
上面定义了两个简单的类,一个是图形类,一个是矩形类;他们之间有简单的继承关系,正确的写法:
Sharp sharp=new Rectangle();
就是说“子类引用可以直接转化成父类引用”,或者说Rectange类和Sharp类之间存在一种安全的隐式转换。
那问题就来了,既然Rectange类和Sharp类之间存在一种安全的隐式转换,那数组Rectange[]和Sharp[]之间是否也存在这种安全的隐式转换呢?
这就牵扯到了将原本类型上存在的类型转换映射到他们的数组类型上的能力,这种能力就称为“可变性(Variance)”。在.NET中,唯一允许可变性的类型转换就是由继承关系带来的“子类引用->父类引用”转换。也就是上面例子所满足的写法。
下例:
Sharp [] sharps=new Rectangle[3];
这是可以的,这说明Rectange[]和Sharp[]之间存在安全的隐式转换。
像这种与原始类型转换方向相同的可变性就称作协变(covariant)
下例:
Rectangle [] rectanges=new Sharp[3];
这是不行,编译不通过,即数组所对应的单一元素的父类引用不可以安全的转化为子类引用。数组也就自然不能依赖这种可变性,达到协变的目的。
所以与协变中子类引用转化为父类引用相反,将父类引用转化为子类引用的就称之为抗变。
即:一个可变性和子类到父类转换的方向一样,就称作协变;而如果和子类到父类的转换方向相反,就叫抗变!
当然可变性远远不只是针对映射到数组的能力,也有映射其它集合的能力如List<T>.
到这里,很多人就会问了,说了这么多,那到底这个协变或者抗变有什么实际利用价值呢?
其价值就在于,在.net 4.0之前可以这么写:
Sharp sharp=new Rectangle();
但是不能这么写:
IEnumerable<Sharp> sharps=newList<Rectangle>();
4.0之后就允许了,因为IEnumerable<T>被声明成如下形式:
public interface IEnumerable<out T>:IEnumerable.
数组是不支持抗变的.在.NET4.0之后,支持协变和抗变的两种类型:泛型接口和泛型委托.
先来看泛型接口中的协变和抗变
定义一个泛型接口:
public interface ICovariant<T>
{}
让上面的两个类各自继承一下该接口:
public class Sharp:ICovariant<Sharp>
{}
public class Rectangle: Sharp,ICovariant<Rectangle>
{}
编写测试代码:
static void Main(string[] args)
{
ICovariant<Sharp> isharp = new Sharp();
ICovariant<Rectangle> irect = new Rectangle();
isharp = irect;
}
发现编译不通过,因为无法将ICovariant<Rectange>隐式转化为ICovariant<Sharp>!
修改接口为:
public interface ICovariant<out T>
{ }
编译顺利通过。这里我为泛型接口的类型参数增加了一个修饰符out,它表示这个泛型接口支持对类型T的协变。
即:如果一个泛型接口IFoo<T>,IFoo<TSub>可以转换为IFoo<TParent>的话,我们称这个过程为协变,而且说“这个泛型接口支持对T的协变”。
那我如果反过来呢,考虑如下代码:
ICovariant<Sharp> isharp = new Sharp();
ICovariant<Rectangle> irect = new Rectangle();
//isharp = irect;
irect = isharp;
编译错误,原因是无法将ICovariant<Sharp>隐式转换为ICovariant<Rectangle>!
修改接口为:
public interface ICovariant<in T>
{ }
编译顺利通过,这里我将泛型接口的类型参数T修饰符修改成in,它表示这个泛型接口支持对类型参数T的抗变。
即:如果有一个泛型接口IFoo<T>,IFoo<TParent>可以转换为IFoo<TSub>的话,我们就称这个过程为抗变,而且说”这个泛型接口支持对T的抗变”.
泛型接口并不单单只有一个参数,所以我们不能简单地说一个接口支持协变还是抗变,只能说一个接口对某个具体的类型参数支持协变或抗变,如ICovariant<out T1,in T2>说明该接口对类型参数T1支持协变,对T2支持抗变。
举个例子就是:ICovariant<Rectange,Sharp>能够转化成ICovariant<Sharp,Rectange>,这里既有协变也有抗变。
以上都是接口并没有属性或方法的情形,接下来给接口添加一些方法:
public interface ICovariant<in T>
{
T Method1();
void Method2(T parm);
}
只是单纯的定义一个接口,却发现编译不通过.而且无论用in还是out都不行,原因是,我把仅有的一个类型参数T即用作了函数的返回值,也用作了函数的参数类型.
(1) 当我用out修饰时,即允许接口对类型参数T协变,也就是满足从ICovariant<Rectangle>到ICovariant<Sharp>的转换,Method1返回值Rectangle到Sharp转换没有任何问题:
ICovariant<Rectangle> irect = new Rectangle();
ICovariant<Sharp> isharp = new Sharp();
isharp = irect;
Sharp sharp=isharp.Method1();
(2) 如果使用in关键字修饰时,允许接口对类型参数T抗变,也就是满足从ICovariant<Sharp>到ICovariant<Rectange>转换:
ICovariant<Rectangle> irect = new Rectangle();
ICovariant<Sharp> isharp = new Sharp();
irect = isharp;
irect.Method2(new Rectangle());
Method2(Sharp)会去替换Method2(Rectange),所以上面的最后一句代码无论以Rectange类型还是Sharp类型为参数都没有任何问题.
综上:在没有额外机制的限制下,接口进行协变或抗变都是类型不安全的。.NET 4.0有了改进,它允许在类型参数的声明时增加一个额外的描述,以确定这个类型参数的使用范围,这个额外的描述即in,out修饰符,它们俩的用法如下:
如果一个类型参数仅仅能用于函数的返回值,那么这个类型参数就对协变相容,用out修饰。而相反,一个类型参数如果仅能用于方法参数,那么这个类型参数就对抗变相容,用in修饰。
所以可以将上面的接口拆成两个接口即可:
public interface ICovariant1<out T>
{
T Method1();
}
public interface ICovariant2<in T>
{
void Method2(T parm);
}
.net中很多接口都仅将参数用于函数返回类型或函数参数类型,如:
public interface IComparable<in T>
public interface IEnumerable<out T>:IEnumerable
几个重要的注意点:
1.仅有泛型接口和泛型委托支持对类型参数的可变性,泛型类或泛型方法是不支持的。
2.值类型不参与协变或抗变,IFoo<int>永远无法协变成IFoo<object>,不管有无声明out。因为.NET泛型,每个值类型会生成专属的封闭构造类型,与引用类型版本不兼容。
3.声明属性时要注意,可读写的属性会将类型同时用于参数和返回值。因此只有只读属性才允许使用out类型参数,只写属性能够使用in参数。
接下来将接口代码改成:
public interface ICovariant<out T>
{
T Method1();
void Method3(IContravariant<T> param);
}
public interface IContravariant<in T>
{
void Method2(T param);
}
我们需要费一些周折来理解这个问题。现在我们考虑ICovariant<Rectange>,它应该能够协变成ICovariant<Sharp>,因为Rectange是Sharp的子类。因此Method3(Rectange)也就协变成了Method3(Sharp)。当我们调用这个协变,Method3(Sharp)必须能够安全变成Method3(Rectange)才能满足原函数的需要(具体原因上面已经示例过了)。这里对Method3的参数类型要求是Sharp能够抗变成Rectange!也就是说,如果一个接口需要对类型参数T协变,那么这个接口所有方法的参数类型必须支持对类型参数T的抗变(如果T有作为某些方法的参数类型)。
同理我们也可以看出,如果接口要支持对T抗变,那么接口中方法的参数类型都必须支持对T协变才行。这就是方法参数的协变-抗变互换原则。所以,我们并不能简单地说out参数只能用于方法返回类型参数,它确实只能直接用于声明返回值类型,但是只要一个支持抗变的类型协助,out类型参数就也可以用于参数类型!(即上面的例子),换句话说,in除了直接声明方法参数类型支持抗变之外,也仅能借助支持协变的类型才能用于方法参数,仅支持对T抗变的类型作为方法参数类型也是不允许的。
既然方法类型参数协变和抗变有上面的互换影响。那么方法的返回值类型会不会有同样的问题呢?
将接口修改为:
public interface IContravariant<in T>
{
}
public interface ICovariant<out T>
{
}
public interface ITest<out T1, in T2>
{
ICovariant<T1> test1();
IContravariant<T2> test2();
}
我们看到和刚刚正好相反,如果一个接口需要对类型参数T进行协变或抗变,那么这个接口所有方法的返回值类型必须支持对T同样方向的协变或抗变(如果有某些方法的返回值是T类型)。这就是方法返回值的协变-抗变一致原则。也就是说,即使in参数也可以用于方法的返回值类型,只要借助一个可以抗变的类型作为桥梁即可。
C#编程(二十九)----------泛型接口的更多相关文章
- 剑指Offer(二十九):最小的K个数
剑指Offer(二十九):最小的K个数 搜索微信公众号:'AI-ming3526'或者'计算机视觉这件小事' 获取更多算法.机器学习干货 csdn:https://blog.csdn.net/baid ...
- Bootstrap <基础二十九>面板(Panels)
Bootstrap 面板(Panels).面板组件用于把 DOM 组件插入到一个盒子中.创建一个基本的面板,只需要向 <div> 元素添加 class .panel 和 class .pa ...
- Web 开发人员和设计师必读文章推荐【系列二十九】
<Web 前端开发精华文章推荐>2014年第8期(总第29期)和大家见面了.梦想天空博客关注 前端开发 技术,分享各类能够提升网站用户体验的优秀 jQuery 插件,展示前沿的 HTML5 ...
- WCF技术剖析之二十九:换种不同的方式调用WCF服务[提供源代码下载]
原文:WCF技术剖析之二十九:换种不同的方式调用WCF服务[提供源代码下载] 我们有两种典型的WCF调用方式:通过SvcUtil.exe(或者添加Web引用)导入发布的服务元数据生成服务代理相关的代码 ...
- VMwarevSphere 服务器虚拟化之二十九 桌面虚拟化之安装View副本服务器
VMwarevSphere 服务器虚拟化之二十九 桌面虚拟化之安装View副本服务器 VMware View中高可用性可是一个必须要考虑的问题.在整个虚拟桌面环境中View Connection S ...
- Bootstrap入门(二十九)JS插件6:弹出框
Bootstrap入门(二十九)JS插件6:弹出框 加入小覆盖的内容,像在iPad上,用于存放非主要信息 弹出框是依赖于工具提示插件的,那它也和工具提示是一样的,是需要初始化才能够使用的 首先我们引入 ...
- mysql进阶(二十九)常用函数
mysql进阶(二十九)常用函数 一.数学函数 ABS(x) 返回x的绝对值 BIN(x) 返回x的二进制(OCT返回八进制,HEX返回十六进制) CEILING(x) 返回大于x的最小整数值 EXP ...
- JAVA之旅(二十九)——文件递归,File结束练习,Properties,Properties存取配置文件,load,Properties的小练习
JAVA之旅(二十九)--文件递归,File结束练习,Properties,Properties存取配置文件,load,Properties的小练习 我们继续学习File 一.文件递归 我们可以来实现 ...
- 【黑金原创教程】【FPGA那些事儿-驱动篇I 】原创教程连载导读【连载完成,共二十九章】
前言: 无数昼夜的来回轮替以后,这本<驱动篇I>终于编辑完毕了,笔者真的感动到连鼻涕也流下来.所谓驱动就是认识硬件,还有前期建模.虽然<驱动篇I>的硬件都是我们熟悉的老友记,例 ...
- 第三百二十九节,web爬虫讲解2—urllib库爬虫—ip代理—用户代理和ip代理结合应用
第三百二十九节,web爬虫讲解2—urllib库爬虫—ip代理 使用IP代理 ProxyHandler()格式化IP,第一个参数,请求目标可能是http或者https,对应设置build_opener ...
随机推荐
- RHEL7 -- 使用Chrony设置时间与时钟服务器同步
Chrony是一个开源的自由软件,它能保持系统时钟与时钟服务器(NTP)同步,让时间保持精确. 它由两个程序组成:chronyd和chronyc. chronyd是一个后台运行的守护进程,用于调整内核 ...
- python文件操作及格式化输出
1 文件与IO 1.1读写文本数据 读写各种不同的文本数据,如ASCII,UTF-8,UTF-9编码等. 使用带有rt模式的open()函数读取文本文件. 例如: with open('db', 'r ...
- mysql 当前时间
1. mysql 获取当前时间 select now() ,current_timestamp(),localtimestamp(),sysdate() ,curdate(),curtime(),u ...
- 查找网内活跃IP和自动传输文本
ifconfig p32p1|egrep -o "broadcast [^ ]*" |grep -o "[0-9.]*"grep -o "broadc ...
- 前端如何在h5页面调用微信支付?
在微信服务号开发的时候经常会遇到微信支付的功能实现,通过实际经验自己总结了一下,前端在H5页面调起微信支付有两种办法,一是利用内置对象,二是通过引用微信的js sdk,亲测都能支付成功,从写法上来看用 ...
- .NetCore中使用ExceptionLess 添加操作日志
上一篇文章已经扩展了日志,下面我们在结合下处理操作日志 通常我们想到操作日志 可能想到的参数可能有 模块 方法 参数内容 操作人 操作时间 操作 Ip 下面我们就来结合这些信息添加操作日志 如果要在代 ...
- 【转载】Java是传值还是传引用
1. 简单类型是按值传递的 Java 方法的参数是简单类型的时候,是按值传递的 (pass by value).这一点我们可以通过一个简单的例子来说明: /* 例 1 */ /** * @(#) Te ...
- 使用matplotlib绘制散点图
在matplotlib中使用函数 matplotlib.pyplot.scatter 绘制散点图,matplotlib.pyplot.scatter的函数签名如下: matplotlib.pyplot ...
- js 高阶函数(map/reduce/filter/sort)
1.map - 方法返回一个新数组,数组中的元素为原始数组元素调用函数处理后的值(注:map不会对空数组进行检测,不会改变原始数组) 语法:array.map(function(currentValu ...
- java集合类TreeMap和TreeSet
看这篇博客前,可以先看下下列这几篇博客 Red-Black Trees(红黑树) (TreeMap底层的实现就是用的红黑 ...