泛型接口

定义

先来看一个简单的例子:

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. jexus配置支持Owin

    vi打开配置文件,加一行 OwinMain=xxx.dll ###################### # Web Site: Default ########################### ...

  2. MXNet深度学习库简介

    MXNet深度学习库简介 摘要: MXNet是一个深度学习库, 支持C++, Python, R, Scala, Julia, Matlab以及JavaScript等语言; 支持命令和符号编程; 可以 ...

  3. On the Bias/Variance tradeoff in Machine Learning

    参考:https://codesachin.wordpress.com/2015/08/05/on-the-biasvariance-tradeoff-in-machine-learning/ 之前一 ...

  4. leetcode 题集

    775. Global and Local Inversions 统计相邻元素组成的逆序对(local inversion)和全局逆序对的数量(global inversion) 思路:local i ...

  5. 2019 CCPC wannfly winter camp Day 5

    C - Division 思路:我们考虑到一点,从大往小取得顺序是不会有问题的,所以可以直接主席树,但是开不下空间,我们可以log分段求. #include<bits/stdc++.h> ...

  6. POJ 3017 Cut the Sequence

    [题目链接] $O(n^2)$ 效率的 dp 递推式:${ dp }_{ i }=min\left( dp_{ j }+\overset { i }{ \underset { x=j+1 }{ max ...

  7. shiro双realm验证

    假设现在有这样一种需求:存在两张表user和admin,分别记录普通用户和管理员的信息.并且现在要实现普通用户和管理员的分开登录,即需要两个Realm——UserRealm和AdminRealm,分别 ...

  8. django 的用户验证及登录状态保持

    一.用户验证功能 Django自带用户验证及登录功能,引入模块为: from django.contrib.auth import authenticate 其中方法authenticate()的接收 ...

  9. HDU 3802Ipad,IPhone

    前两块可以看成是不是二次剩余,快速幂计算即可. 后半部分可以看成x1=a+b+2ab,x2=a+b-2ab为特征方程x^2-px-qx=0的两根 然后可以通过韦达定理求出p和q,因此递推式为A(n+2 ...

  10. play framework系列之maven 构建

    一,文章内容 1,Play framwwork 的好处就是构建java 项目简单快速.经过下边几步即可完成. play new testSimpleMvn 选择2 java 项目即可. 然后进入到 t ...