泛型接口

定义

先来看一个简单的例子:

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#编程(二十九)----------泛型接口的更多相关文章

  1. 剑指Offer(二十九):最小的K个数

    剑指Offer(二十九):最小的K个数 搜索微信公众号:'AI-ming3526'或者'计算机视觉这件小事' 获取更多算法.机器学习干货 csdn:https://blog.csdn.net/baid ...

  2. Bootstrap <基础二十九>面板(Panels)

    Bootstrap 面板(Panels).面板组件用于把 DOM 组件插入到一个盒子中.创建一个基本的面板,只需要向 <div> 元素添加 class .panel 和 class .pa ...

  3. Web 开发人员和设计师必读文章推荐【系列二十九】

    <Web 前端开发精华文章推荐>2014年第8期(总第29期)和大家见面了.梦想天空博客关注 前端开发 技术,分享各类能够提升网站用户体验的优秀 jQuery 插件,展示前沿的 HTML5 ...

  4. WCF技术剖析之二十九:换种不同的方式调用WCF服务[提供源代码下载]

    原文:WCF技术剖析之二十九:换种不同的方式调用WCF服务[提供源代码下载] 我们有两种典型的WCF调用方式:通过SvcUtil.exe(或者添加Web引用)导入发布的服务元数据生成服务代理相关的代码 ...

  5. VMwarevSphere 服务器虚拟化之二十九 桌面虚拟化之安装View副本服务器

    VMwarevSphere 服务器虚拟化之二十九  桌面虚拟化之安装View副本服务器 VMware View中高可用性可是一个必须要考虑的问题.在整个虚拟桌面环境中View Connection S ...

  6. Bootstrap入门(二十九)JS插件6:弹出框

    Bootstrap入门(二十九)JS插件6:弹出框 加入小覆盖的内容,像在iPad上,用于存放非主要信息 弹出框是依赖于工具提示插件的,那它也和工具提示是一样的,是需要初始化才能够使用的 首先我们引入 ...

  7. mysql进阶(二十九)常用函数

    mysql进阶(二十九)常用函数 一.数学函数 ABS(x) 返回x的绝对值 BIN(x) 返回x的二进制(OCT返回八进制,HEX返回十六进制) CEILING(x) 返回大于x的最小整数值 EXP ...

  8. JAVA之旅(二十九)——文件递归,File结束练习,Properties,Properties存取配置文件,load,Properties的小练习

    JAVA之旅(二十九)--文件递归,File结束练习,Properties,Properties存取配置文件,load,Properties的小练习 我们继续学习File 一.文件递归 我们可以来实现 ...

  9. 【黑金原创教程】【FPGA那些事儿-驱动篇I 】原创教程连载导读【连载完成,共二十九章】

    前言: 无数昼夜的来回轮替以后,这本<驱动篇I>终于编辑完毕了,笔者真的感动到连鼻涕也流下来.所谓驱动就是认识硬件,还有前期建模.虽然<驱动篇I>的硬件都是我们熟悉的老友记,例 ...

  10. 第三百二十九节,web爬虫讲解2—urllib库爬虫—ip代理—用户代理和ip代理结合应用

    第三百二十九节,web爬虫讲解2—urllib库爬虫—ip代理 使用IP代理 ProxyHandler()格式化IP,第一个参数,请求目标可能是http或者https,对应设置build_opener ...

随机推荐

  1. 初始ASP.NET数据控件GridView

    使用GridView控件绑定数据源 GridView控件个人认为就是数据表格控件,它以表格的形式显示数据源中的数据.每列表示一个字段,每行表示一条记录.     GridView控件支持在页面有一下功 ...

  2. 浅谈js设计模式 — 命令模式

    命令模式最常见的应用场景是:有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么.此时希望用一种松耦合的方式来设计程序,使得请求发送者和请求接收者能够消除彼此之间的耦 ...

  3. ASP.NET Web配置使用HTTPS实用案例

    Step by Step 配置使用HTTPS的ASP.NET Web应用 有关HTTPS.SSL以及SSL证书的工作原理,参见 <HTTPS那些事(一)HTTPS原理> <HTTPS ...

  4. ASP.Net1

    一.Web应用程序与传统桌面应用程序的不同: 1.产品级的Web应用程序总是包括至少两台联网的机器:一台承载网站,另一台在Web浏览器中查看数据. 即:我们通过自己的电脑浏览Web程序,这个程序会向服 ...

  5. MapReduce原理1

    Mapreduce是一个分布式运算程序的编程框架,是用户开发“基于hadoop的数据分析应用”的核心框架: Mapreduce核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算 ...

  6. Python学习笔记之函数式编程

    python中的高阶函数 高阶函数就是 变量名指向函数,下面代码中的变量abs其实是一个函数,返回数字的绝对值,如abs(-10) 返回 10 def add(x,y,f): return f(x) ...

  7. 使用SOCKET获取网页的内容

    使用fsockopen()函数来实现获取页面信息,完整代码如下 //设置字符集(由于要抓取的网易网站字符集编码是gbk编码) header("content-type:text/html;c ...

  8. 深入理解Git - 一切皆commit

    在对 git 有了基本理解和知道常规操作之后,如何对 git 的使用有进一步的理解? 一切皆 commit 或许是个不错的理解思路. 本文将从『一切皆 commit 』的角度,通过 git 中常见的名 ...

  9. error 1044 (42000):access denied for user ''@'localhost' to database 'quickapp' 解决方法

    在虚拟机上重新创建一个数据库时,一直出现这个报错:error 1044 (42000):access denied for user ''@'localhost' to database 'quick ...

  10. rabbitmq学习(八) —— 可靠机制上的“可靠”

    接着上一篇,既然已经有了手动ack.confirm机制.return机制,还不够吗? 以下博文转自https://www.jianshu.com/p/6579e48d18ae和https://my.o ...