源码下载

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

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

 namespace LSP
{
public class Bird
{
public virtual void Show()
{
Console.WriteLine("It's me, bird.");
}
}
}

Bird

 namespace LSP
{
public class Swan : Bird
{
public override void Show()
{
Console.WriteLine("It's me, swan.");
}
}
}

Swan

 namespace LSP
{
public class Program
{
static void Main(string[] args)
{
Bird bird = new Swan();
bird.Show();
Console.ReadLine();
}
}
}

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>。

好了,重点来了!

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

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

 public class Program
{
public static void Main(string[] args)
{
List<object> obj = null;
List<string> str = null; /* Error:
* Cannot implicitly convert type
* 'System.Collections.Generic.List<string>'
* to 'System.Collections.Generic.List<object>'
*/ //obj = str; Console.ReadLine();
}
}

VarianceList

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

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

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

 namespace Invariance
{
public class Invariance<T>
{
T Test(T t)
{
return default(T);
}
}
}

Variance<T>

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

 namespace Invariance
{
public class Program
{
public static void Main(string[] args)
{
Invariance<object> invarianceObj = new Invariance<object>();
Invariance<string> invaricaceStr = new Invariance<string>(); //invarianceObj = invaricaceStr;
//invaricaceStr = invarianceObj; Console.ReadLine();
}
}
}

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关键字就可以了。

 namespace Covariance
{
public interface ITest<out T>
{
T Test();
}
}

ITest<out T>

 namespace Covariance
{
public class Program
{
public static void Main(string[] args)
{
ITest<object> obj = null;
ITest<string> str = null;
obj = str; IEnumerable<object> enuObj = null;
IEnumerable<string> enuStr = null;
enuObj = enuStr;
}
}
}

Covariance

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

五、逆变性(Contravariance)

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

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

 namespace Contravariance
{
public interface ITest<in T>
{
void Test(T t);
}
}

ITest<in T>

 namespace Contravariance
{
public class Program
{
public static void Main(string[] args)
{
ITest<object> obj = null;
ITest<string> str = null;
str = obj; IComparable<object> comObj = null;
IComparable<string> comStr = null;
comStr = comObj;
}
}
}

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. boost:exception使用实例

    /************************************************************************/ /*功能描述: boost exception使用 ...

  2. 深入浅出Nodejs读书笔记(转)

    Node简介 这一章简要介绍了Node,从中可以了解Node的发展历程及其带来的影响和价值. 为什么叫Node?起初,Ryan Dahl称他的项目为web.js,就是一个Web服务器,但是项目的发展超 ...

  3. Android IOS WebRTC 音视频开发总结(六四)-- webrtc能走多远我不知道,但这个市场真实存在

    本文主要总结目前都有哪些使用场景用到webrtc,文章最早发表在我们的微信公众号上,详见这里, 欢迎关注微信公众号blackerteam,更多详见www.blackerteam.com webrtc只 ...

  4. atexit注册的函数是在main函数之后执行?

    跟atexit函数相识已久,man手册里对atexit的解释是这么一段: The atexit() function registers the given function to be called ...

  5. BochsDebug

    Bochs User Manual   Chapter 8. Tips and Techniques   8.12. Using Bochs internal debugger Note, if yo ...

  6. linux下securetty文件

    “/etc/securetty”文件允许你规定“root”用户可以从那个TTY设备登录.登录程序(通常是“/bin/login”)需要读取“/etc/securetty”文件.它的格式是:列出来的tt ...

  7. Mysql连接测试代码

    <?php $link=mysql_connect('localhost','root','htuidc'); if($link) echo "success"; ?>

  8. js实现文字字幕滚动

    <div class="dggd_r" id="h" style="height:400px;overflow:hidden;display:i ...

  9. jQuery 遍历each()的使用方法

    .each()是一个for循环的包装迭代器.each()通过回调的方式处理,并且会有2个固定的实参,索引与元素(从0开始计数).each()回调方法中的this指向当前迭代的dom元素 遍历方法: & ...

  10. mysql批量修改表引擎

    生成修改的语句 SELECT CONCAT('ALTER TABLE ',table_name,' ENGINE=InnoDB;') FROM information_schema.tables WH ...