源码下载

一、里氏替换原则(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. JS常用的设计模式(13)——组合模式

    组合模式又叫部分-整体模式,它将所有对象组合成树形结构.使得用户只需要操作最上层的接口,就可以对所有成员做相同的操作. 一个再好不过的例子就是jquery对象,大家都知道1个jquery对象其实是一组 ...

  2. Android knock code analysis

    My colleague she forgot the knock code and ask me for help. I know her phone is LG G3 D855 with Andr ...

  3. javascript 详解数组

      概念 数组创建 数组读写 数组 VS. 一般对象 相同点 不同点 稀疏数组 数组的length属性 元素增删 数组迭代 二维数组 数组方法 Array.prototype.join Array.p ...

  4. Promise机制

    Promise的诞生与Javascript中异步编程息息相关,js中异步编程主要指的是setTimout/setInterval.DOM事件机制.ajax,通过传入回调函数实现控制反转.异步编程为js ...

  5. SQLserver2012 修改数据库架构

    还原数据库以后,发现有一张表的架构不对,执行sql提示:对象名无效.

  6. centos6.2下安装redis和phpredis扩展,亲测好用

    安装redis: 下载:http://www.redis.io/download redis-2.6.2.tar.gz ]# tar -zxf redis-2.6.2.tar.gz ]# cd red ...

  7. IOS基础——静态方法(类方法)和实例方法

    1.实例方法/动态方法 a).标识符:- b).调用方式:(实例对象    函数) c).实例方法在堆栈上. 2.静态方法/类方法 a).标识符:+ b).调用方式:(类    函数) c).静态方法 ...

  8. C#winform导出数据到Excel的类

    /// <summary> /// 构造函数 /// </summary> public ExportData() { } /// <summary> /// 保存 ...

  9. URL的语法及HTTP报文

    大多数URL方案的URL语法都建立在这个由9部分构成的通用格式上: scheme://user:password@host:port/path;params?query#frag 方案:http或者h ...

  10. 自己一晚上总结的php基础知识!好累。好充实。

    为了巩固自己的基础提升自己的技术.花了一晚上的时间结合w3c上的非常基础的东西,和自己的部分见解,写了不少,望大神们指正,指导.. <?php /* 这段话必须要写在开篇啊!死老猫,你又刺激我! ...