• 不变性、协变性和逆变性(Invariance, Covariance & Contravariance)


    源码下载

    一、里氏替换原则(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)。

  • 相关阅读:
    【转载】USB2.0接口差分信号线设计
    2.4G高频PCB天线设计
    [转]热插拔原理和应用
    [转]or cad drc 错误
    Chrome浏览器任意修改网页内容
    Oracle笔记之约束
    Oracle笔记之表空间
    Oracle笔记之序列(Sequence)
    Oracle笔记之用户管理
    Win7下SQLPlus登录时报错"SP2-1503:无法初始化Oracle调用界面"
  • 原文地址:https://www.cnblogs.com/Figgy/p/4575719.html
Copyright © 2020-2023  润新知