zoukankan      html  css  js  c++  java
  • 不变性、协变性和逆变性(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)。

  • 相关阅读:
    WCF Server Console
    Restart IIS With Powershell
    RestartService (recursively)
    Copy Files
    Stopping and Starting Dependent Services
    多线程同步控制 ManualResetEvent AutoResetEvent MSDN
    DTD 简介
    Using Powershell to Copy Files to Remote Computers
    Starting and Stopping Services (IIS 6.0)
    java中的NAN和INFINITY
  • 原文地址:https://www.cnblogs.com/Figgy/p/4575719.html
Copyright © 2011-2022 走看看