源码下载

一、里氏替换原则(Liskov Substitution Principle LSP)

  我们要讲的不是协变性和逆变性(Covariance & Contravariance)吗?是的,没错。但先不要着急,在这之前,我们有必要再回味一下LSP。废话不多说,直接上代码:

  1. namespace LSP
  2. {
  3. public class Bird
  4. {
  5. public virtual void Show()
  6. {
  7. Console.WriteLine("It's me, bird.");
  8. }
  9. }
  10. }

Bird

  1. namespace LSP
  2. {
  3. public class Swan : Bird
  4. {
  5. public override void Show()
  6. {
  7. Console.WriteLine("It's me, swan.");
  8. }
  9. }
  10. }

Swan

  1. namespace LSP
  2. {
  3. public class Program
  4. {
  5. static void Main(string[] args)
  6. {
  7. Bird bird = new Swan();
  8. bird.Show();
  9. Console.ReadLine();
  10. }
  11. }
  12. }

Program

根据里氏替换原则,任何基类可以出现的地方,子类一定可以出现。

因为Swan类继承于Bird类,所以“Bird bird=new Bird();”中,我需要创建一个Bird对象,你给了我一个Swan对象是完全可行的。通俗地讲,我要你提供鸟类动物给我,你给我一只天鹅,当然没有问题。

然而,我们在调用bird的Show方法时,发生了什么呢?

Bird类和Swan类中都有Show方法,调用这个方法时,编译器是知道这个bird实际指向的Swan对象的。它会先查看Swan本身是不是有同签名的方法,如果有就直接调用。如果没有再往Swan的父类里查看,如果再没有,再往上面找,直到找到为止。如果最终也没有找到,就会报错。

所以,我们看到程序调用的是Swan的Show方法:"It's me, swan."

二、协变和逆变是什么?

关于这个,我们还是先看看官方的解释:

协变和逆变都是术语,前者指能够使用比原始指定的派生类型的派生程度更大(更具体的)的类型,后者指能够使用比原始指定的派生类型的派生程度更小(不太具体的)的类型。

看了是不是有种“懂的依然懂,不懂的依然不懂的感觉”?

简单地说,

协变:你可以用一个子类对象去替换相应的一个父类对象,这是完全符合里氏替换原则的,和协(谐)的变。如:用Swan替换Bird。

逆变:你可以用一个父类对象去替换相应的一个父类对象,这貌似不符合里氏替原则的,不和协(谐)的逆变。如:用Bird替换Swan。

那么事实真的如此吗?协变是不是比逆变更合理?其实他们完全就是一回事,都是里氏替换原则的一种表现形式罢了。

三、不变性(Invariance)

我们知道:Bird bird=new Swan();是没有问题的。

那么对于泛型,List<Bird> birds=List<Swan>();是不是也OK呢?

No!

首先,因为.Net Framework只向泛型接口和委托提供了协变和逆变的便利。

再者,想要实现协变或逆变,也得在语法上注明out(协变)或in(逆变)。

对于这类不支持协变和逆变的情况,我们称为不变性(Invariance)。为了维持泛型的同质性(Homogeneity),编译器禁止将List<Swan>隐式或显式地转换为List<Bird>。

好了,重点来了!

为什么要这样?这样,很不方便。而且,看起来也不符合里氏替换原则。

简单地说,维持同质性,不允许这样的转换,还是为了编译正常。什么是编译正常,就是别给咱报错。

  1. public class Program
  2. {
  3. public static void Main(string[] args)
  4. {
  5. List<object> obj = null;
  6. List<string> str = null;
  7.  
  8. /* Error:
  9. * Cannot implicitly convert type
  10. * 'System.Collections.Generic.List<string>'
  11. * to 'System.Collections.Generic.List<object>'
  12. */
  13.  
  14. //obj = str;
  15.  
  16. Console.ReadLine();
  17. }
  18. }

VarianceList

如代码注解的那样,“obj=str;”编译器会报错:

Error :Cannot implicitly convert type 'System.Collections.Generic.List<string>' to 'System.Collections.Generic.List<object>'

List<T>是微软提供给我们的,里面封闭太多东西,不方便分析,我们就自己动手来写一个泛型类Invariance<T>。

  1. namespace Invariance
  2. {
  3. public class Invariance<T>
  4. {
  5. T Test(T t)
  6. {
  7. return default(T);
  8. }
  9. }
  10. }

Variance<T>

写好了泛型类,我们再来试一试。

  1. namespace Invariance
  2. {
  3. public class Program
  4. {
  5. public static void Main(string[] args)
  6. {
  7. Invariance<object> invarianceObj = new Invariance<object>();
  8. Invariance<string> invaricaceStr = new Invariance<string>();
  9.  
  10. //invarianceObj = invaricaceStr;
  11. //invaricaceStr = invarianceObj;
  12.  
  13. Console.ReadLine();
  14. }
  15. }
  16. }

Variance<T> Test

"invarianceObj = invaricaceStr;"报错:

Error : Cannot implicitly convert type 'Invariance.Invariance<string>' to 'Invariance.Invariance<object>'

“invaricaceStr = invarianceObj;”报错:

Error : Cannot implicitly convert type 'Invariance.Invariance<object>' to 'Invariance.Invariance<string>'

讲到这么多报错,还是没讲到核心,为什么要报错。

我们可以假设,如果不报错,运行起来会是怎样:Invariance<T>类型参数T是在使用时,确定具体类型的。

先来说貌似符合里氏替换原则的情况,

Invariance<object> invarianceObj =new Invariance<string>();

用string替换object没有问题。但这个语句表达的不仅仅是用string来替换object,也表示用object来替换string。

关键在于类型参数,是在泛型类中使用的,我们不敢保证他是否于参数还是返回值。

如:Invariance<object> invarianceObj调用Test(object obj),传入的是自身的类型参数,而实际执行时,是执行实际指向的对象Invariance<string> invarianceStr的Test(string str)方法。很明显,Invariance<string> invariance的Test(string str)方法需要接收一个string类型的参数,得到却是一个object。这是不合法的。

那是不是反过来就可以了呢?

Invariance<string> invaricaceStr=new Invariance<object>();

这样,你实际执行方法时,需要一个object类型的参数,我给你一个string总没问题了吧。

OK,这样完全没有问题。

然而,不要忘了,方法可能不只是有参数,还可能有返回值。

参数:Invariance<string> invaricaceStr调用Test(string str),将string传给invarianceObj的Test(object obj)方法。目前为止,OK。

返回值:Invariance<string> invaricaceStr要求Test(string str)返回一个string对象。而实际执行方法的invarianceObj却只能保证返回一个object对象。NG!

看到了吧。这就是为什么.Net Framework要保持类型参数的同质性,而不允许T类型参数,哪怕从子类到父类或父类到子类的任何一种转换。

因为你只能保证参数或返回值,其中一项转换成功。

四、协变性(Covariance)

理解了为什么要坚持不变性,理解起协变性就容易多了。如果我能在泛型接口或者委托中保证,我的类型参数,只能外部取出,不允许外部传入。那么就不存在上面讲的将类型参数作为参数传入方法的情况了。

怎么保证?只需要在类型参数前加out关键字就可以了。

  1. namespace Covariance
  2. {
  3. public interface ITest<out T>
  4. {
  5. T Test();
  6. }
  7. }

ITest<out T>

  1. namespace Covariance
  2. {
  3. public class Program
  4. {
  5. public static void Main(string[] args)
  6. {
  7. ITest<object> obj = null;
  8. ITest<string> str = null;
  9. obj = str;
  10.  
  11. IEnumerable<object> enuObj = null;
  12. IEnumerable<string> enuStr = null;
  13. enuObj = enuStr;
  14. }
  15. }
  16. }

Covariance

注:interface IEnumerable<out T>是微软提供的支持协变的泛型接口之一。

五、逆变性(Contravariance)

与逆变性类似,如果我能在泛型接口或者委托中保证,我的类型参数,只能作为参数从外部传入,不允许将其取出。那么就不存在将类型参数作为返回值返回的情况了。

同样,我们只需要在类型参数前加in关键字就可以了。

  1. namespace Contravariance
  2. {
  3. public interface ITest<in T>
  4. {
  5. void Test(T t);
  6. }
  7. }

ITest<in T>

  1. namespace Contravariance
  2. {
  3. public class Program
  4. {
  5. public static void Main(string[] args)
  6. {
  7. ITest<object> obj = null;
  8. ITest<string> str = null;
  9. str = obj;
  10.  
  11. IComparable<object> comObj = null;
  12. IComparable<string> comStr = null;
  13. comStr = comObj;
  14. }
  15. }
  16. }

Contravariance

注:interface IComparable<in T>是微软提供的支持逆变的泛型接口之一。

后记:常常只是在博客园看大神们的文章,自己总是不敢出声,第一次在这里写东西,有理解错误的地方,恳请批评指正(QQ:582043340)。

不变性、协变性和逆变性(Invariance, Covariance & Contravariance)的更多相关文章

  1. Java 泛型 协变性、逆变性

    Java 泛型 协变性.逆变性 @author ixenos 摘要:协变性.协变通配符.协变数组.协变返回值 协变性.逆变性和无关性 在面向对象的计算机程序语言中,经常涉及到类型之间的转换,例如从具体 ...

  2. oc 的 协变性与逆变性

    ?协变性与逆变性是类型关系在范畴论的定义.是类型的继承关系在高阶类型中的定义? __kindof只是在统一继承体系下方便了类型转化,提供了使用时语法上的便捷:但是对于类型转换是否正确不做判定: kin ...

  3. JQuery选择器大全 前端面试送命题:面试题篇 对IOC和DI的通俗理解 c#中关于协变性和逆变性(又叫抗变)帮助理解

    JQuery选择器大全   jQuery 的选择器可谓之强大无比,这里简单地总结一下常用的元素查找方法 $("#myELement")    选择id值等于myElement的元素 ...

  4. c#中关于协变性和逆变性(又叫抗变)帮助理解

    今天回忆了之前看的<深入理解C#>这本书中的泛型章节,其中对泛型的可变性的理解.泛型可变性分两种:协变和逆变.逆变也又称为抗变. 怎么理解这两个名词的意思: ①:协变即为在泛型接口类型中使 ...

  5. C#中的斜变性和逆变性的详解

    1,问题 大家可以看到定义泛型类型的可以看到out和in这两个关键字,那么具体代表什么意思呢? 2,文字解释 C# 4.0通过两个关键字:out和in来分别支持以协变和逆变的方式使用泛型. 如果某个返 ...

  6. C#4.0特性

    C# 4.0的主要主题是动态编程.对象的意义变得越来越“动态”,它们的结构和行为无法通过静态类型来捕获,或者至少编译器在编译程序时无法得知对象的结构和行为. a. 来自动态编程语言——如Python或 ...

  7. Java中的逆变与协变

    看下面一段代码 Number num = new Integer(1); ArrayList<Number> list = new ArrayList<Integer>(); ...

  8. Java中的逆变与协变(转)

    看下面一段代码 Number num = new Integer(1); ArrayList<Number> list = new ArrayList<Integer>(); ...

  9. Java中的逆变与协变 专题

    结论先行: PECS总结: 要从泛型类取数据时,用extends: 协变 要往泛型类写数据时,用super: 逆变 既要取又要写,就不用通配符(即extends与super都不用) 不变 List&l ...

随机推荐

  1. 问 如何使用css将select的边框以及右边的小三角形去掉?

    最好css2,css3都给出解决方案,效果如下: CSS2 只能使用div和ul进行模拟了,结构很简单,具体可参考Alice的 button-dropdownCSS3 可以使用CSS3的属性appea ...

  2. 使用Git上传本地项目代码到github

    前提:(1)ssh密钥(让本地与git链接) &  (2)装好gitbash 1.git中创建好库 2.文件夹中输入:git init (出现隐藏的.git文件) 3.git remote a ...

  3. Android IOS WebRTC 音视频开发总结(十四)-- sip和xmpp异同

    这篇文章主要介绍XMPP与SIP,很多人容易混淆这两个概念,转载请说明出处(博客园RTC.Blacker). 简介:XMPP和SIP都是应用层协议,主要用于互联网上发送语音和即时通讯. SIP在RFC ...

  4. Nginx 下无法读取session 导致 thinkphp验证码错误

    打开php配置文件 php.ini 使用搜索命令 whereis php.ini 一般在:/etc/php.ini 目录下 使用vim命令打开 找到: session.save_path 找到php保 ...

  5. WinForm中使用AnyCAD三维控件 の 初始化

    在WinForm中可以方便的集成AnyCAD.Net三维控件,只需要以下几部即可完成. 一.添加DLL程序集 AnyCAD.Foundation.Net.dll AnyCAD.Presentation ...

  6. linux环境配置

    一.JDK安装 1.通过xftp工具把jdk-8u60-linux-x64.gz上传到linux 2.解压JDK命令tar -xzf jdk-8u60-linux-x64.gz 3.linux配置环境 ...

  7. 1.Mariadb(mysql)基本操作

    1.:安装与初始化 1)安装 yum install -y mariadb\* 2)初始化 systemctl restart mariadb systemctl enable  mariadb my ...

  8. 视频特效制作:如何给视频添加边框、水印、动画以及3D效果

    2014-12-08 09:47 编辑: suiling 分类:iOS开发 来源:叶孤城的blog 招聘信息: iOS手机软件开发工程师 iOS工程师 Web后端高级开发工程师 iOS软件工程师 ja ...

  9. 泛型集合转换为DataTable

    在做项目中,遇到了将集合转换为DataTable的使用,在网上看了资料,在这里记录下来,分享. using System; using System.Collections.Generic; usin ...

  10. Environment类,获取程序所在机器信息

    一.属性 CommandLine  获取该进程的命令行.CurrentDirectory 获取或设置当前工作目录的完全限定路径.ExitCode 获取或设置进程的退出代码.HasShutdownSta ...