zoukankan      html  css  js  c++  java
  • 协变与逆变详细解读

    协变与逆变是.Net4.0新加入的概念,我看了很多博客文章,可能是我悟性比较差,感觉没有完全讲明白,自己研究了一天终于搞懂了,特此记录一下。

    一、简单理解协变和逆变

    //协变:子类对象(引用)赋值给父类变量(引用)
    object obj = null;
    string str = "";
    obj = str;
    //逆变:父类对象(引用)赋值给子类变量(引用)
    object obj = null;
    string str = "";
    str = obj;
    /*
    str = obj;这段代码大家会发现是错误的,这个赋值是基本操作都是错误的,
    那又如何实现逆变这种逆反的赋值操作呢?实际上逆变根本不是逆向将父类
    赋给子类的,我慢慢解释...
    */

    二、真正的协变和逆变

    概念:

    1、以前的泛型系统(或者说没有in/out关键字时),是不能“变”的,无论是“逆”还是“顺(协)”。
    2、当前仅支持接口和委托的逆变与协变 ,不支持类和方法。但数组也有协变性。
    3、值类型不参与逆变与协变。

    协变:Foo<ParentClass> = Foo<ChildClass>

    public class TestOut<T> where T : new()
    {
        /*
         * 关键字out因为协变的类型T只能作为输出参数使用,而不能作为输入参数
         */
        //*****[协变]泛型委托Demo*****
        //1、创建泛型委托
        public delegate T MyFunA<T>();     //默认不支持协变与逆变
        public delegate T MyFunB<out T>(); //设置支持协变
        //public delegate void MyFunC<out T>(T param);//错误,协变类型只能“出”不能“入”
        //2、创建委托变量
        public MyFunA<object> FunAObject = null;
        public MyFunA<string> FunAString = null;
        public MyFunB<object> FunBObject = null;
        public MyFunB<string> FunBString = null;
        public MyFunB<int> FunBInt = null;
        //3、验证结果
        public void TestFun()
        {
            //FunAObject = FunAString;//错误,不可用协变
            FunBObject = FunBString;//正确,可用协变可以完成子类string到父类object的转换
            //FunBObject = FunBInt;   //错误,值类型不参与协变
        }
    
        //*****[协变]泛型接口Demo*****
        //1、创建泛型接口
        public interface IMyInterfaceA<T> { }       //默认不支持协变与逆变
        public interface IMyInterfaceB<out T> { }   //设置支持协变
        //public interface IMyInterfaceC<out T>
        //{
        //    void Test(T param);//错误,协变只能“出”不能“入”
        //}
        //2、创建接口变量
        public IMyInterfaceA<object> interAObject = null;
        public IMyInterfaceA<string> interAString = null;
        public IMyInterfaceB<object> interBObject = null;
        public IMyInterfaceB<string> interBString = null;
        public IMyInterfaceB<int> interBInt = null;
        //3、验证结果
        public void TestInterface()
        {
            //interAObject = interAString;    //错误,不可用协变
            interBObject = interBString;    //正确,可用协变可以完成子类string到父类object的转换
            //interBObject = interBInt;       //错误,值类型不参与协变
        }
    }

    逆变:Foo<ChildClass> = Foo<ParentClass>

    public class TestIn<T> where T : new()
    {
        /*
         * 关键字in因为逆变的类型只能作为输入参数,而不能作为输出参数
         */
        //*****[逆变]泛型委托*****
        //1、创建泛型委托
        public delegate void MyActionA<T>(T param);     //默认不支持协变与逆变
        public delegate void MyActionB<in T>(T param);  //设置支持逆变
        //public delegate T MyActionC<in T>();//错误,逆变只能“入”不能“出”
        //2、创建委托变量
        public MyActionA<object> ActionAObject = null;
        public MyActionA<string> ActionAString = null;
        public MyActionB<object> ActionBObject = null;
        public MyActionB<string> ActionBString = null;
        public MyActionB<int> ActionBInt = null;
        //3、验证结果
        public void TestAction()
        {
            //ActionAString = ActionAObject;  //错误,不可用逆变
            ActionBString = ActionBObject;  //正确,可用逆变可以完成从父类object到子类string的转换
            //ActionBInt = ActionBObject;     //错误,值类型不参与逆变
        }
    
        //*****[逆变]泛型接口*****
        //1、创建泛型接口
        public interface IMyInterfaceA<T> { }     //默认不支持协变与逆变
        public interface IMyInterfaceB<in T> { }  //设置支持逆变
        //public interface IMyInterfaceC<in T>
        //{
        //    T Test();//错误,逆变只能“入”不能“出”
        //}
        //2、创建接口变量
        public IMyInterfaceA<object> interAObject = null;
        public IMyInterfaceA<string> interAString = null;
        public IMyInterfaceB<object> interBObject = null;
        public IMyInterfaceB<string> interBString = null;
        public IMyInterfaceB<int> interBInt = null;
        //3、验证结果
        public void TestInterface()
        {
            //interAString = interAObject;    //错误,不可用逆变
            interBString = interBObject;    //正确,可用你变可以完成从父类object到子类string的转换
            //interBInt = interBObject;       //错误,值类型不参与逆变
        }
    }

    三、解析协变与逆变(协变是顺序的,逆变并不是逆反的)

    这里就是我看别人的博客没有看懂的地方,研究时从这里卡住了半天,想通后发现豁然开朗,现在分享出来

    先创建一个协变接口,一个逆变接口

    //*****协变接口*****
    public interface ITestA<out T>
    {
        T Test();
    }
    public class TestA<T> : ITestA<T>
    {
        public T Test()
        {
            //do something...
            return default(T);
        }
    }
    //*****逆变接口*****
    public interface ITestB<in T>
    {
        void Test(T p);
    }
    public class TestB<T> : ITestB<T>
    {
        public void Test(T p)
        {
            //do something...
        }
    }

    协变解析:

    internal class Program
    {
        private static void Main(string[] args)
        {
            //写法一
            ITestA<object> testA = new TestA<string>();
            object obj = testA.Test();
            //写法二
            ITestA<object> testA1 = null;
            ITestA<string> testA2 = null;
            testA1 = testA2;
            obj = testA1.Test();
    
            /*
            执行步骤如下:
            //先调用父类函数
            public object ITestA<object>.Test()
            {
                //发现父类函数为接口,函数体由子类实现,所以...
                //再调用子类函数
                public string ITestA<string>.Test()
                {
                    //do something...   
                }
                //父类函数调用子类函数,子类函数向外return返回值,由string类型传至object类型
            }
            */
            //协变“可出不可入”因为由子类函数向父类函数返回值,子类类型小,父类类型大,所以可以进行安全转换
        }
    }

    别的博客中看到以上解释,没看明白,后来才懂,他的意思是:协变时是子类向父类返回值,值类型是由子到父,可以安全转换!
    [原式就是主观应该调用的方式,我想调用子类的这个函数]
    [变式就是实际运行时调用的方式,先调用父类函数再由父类函数调用子类函数]

    //ITest1<object> = ITest1<string>
    //原式(子类):string ITest1<string>.Test()
    ////变式(父类):object ITest1<object>.Test()

    逆变解析:

    internal class Program
    {
        private static void Main(string[] args)
        {
            //写法一
            ITestB<string> testB = new TestB<object>();
            testB.Test("");
            //写法二
            ITestB<string> testB1 = null;
            ITestB<object> testB2 = null;
            testB1 = testB2;
            testB1.Test("");
    
            /*
            执行步骤如下:
            //先调用父类函数
            public void ITestB<string>.Test(string param)
            {
                //发现父类函数为接口,函数体由子类实现,所以...
                //再调用子类函数
                public void ITestB<object>.Test(object param)
                {
                    //do something...   
                }
                //父类函数调用子类函数,并向子类函数传递参数由string类型传至object类型
            }
            */
            //逆变“可入不可出”因为由父类函数向子类函数传递参数,父类类型小,子类类型大,所以可以进行安全转换
        }
    }

    别的博客中看到以上解释,没看明白,后来才懂,他的意思是:逆变时是父类向子类传参数值,值类型是由子到父,可以安全转换!
    [原式就是主观应该调用的方式,我想调用子类的这个函数]
    [变式就是实际运行时调用的方式,先调用父类函数再由父类函数调用子类函数]

    //ITest2<string> = ITest2<object>
    //原式(子类):void ITest2<object>.Test(object param)
    ////变式(父类):void ITest2<string>.Test(string param)

    调用执行步骤:
    ITestA<object> testA = new TestA<string>();
    object obj = testA.Test();
    ITestB<string> testB = new TestB<object>();
    testB.Test("");
    父类变量(引用)调用方法,实际上执行步骤如下:
    1、调用父类自己的方法
    2、被告知方法体由子类实现
    3、父类去调用子类方法
    4、【逆变】发现子类方法有参,于是父类传递自己的参数(类型string)到子类(类型object),可以安全转换
    5、子类执行方法体功能
    6、【协变】将执行的返回值返回给父类
    7、【协变】父类接收子类方法返回值,返回值类型为子类的
    8、【协变】继续向上返回,发现返回值类型不一样(类型string),所以转为父类方法的类型返回(类型object),可以安全转换

    所以这就是为什么【协变只能返回值】,而【逆变只能传递值】,实际协变逆变并没有父类型转子类型的过程,都是使用的子类型转父类型的安全转换

    应用场景:微软提倡只要是泛型的接口或者委托都希望使用协变逆变,RedSharper也会有相应的提示,这样做也可以增加【函数传入参数值、【函数返回值】的扩展性,何乐而不为呢~

  • 相关阅读:
    51nod 1117 聪明的木匠:哈夫曼树
    51nod 1010 只包含因子2 3 5的数
    51nod 2636 卡车加油
    51nod 2989 组合数
    51nod 2652 阶乘0的数量 V2
    51nod 1103 N的倍数
    51nod 2489 小b和灯泡
    51nod 1003 阶乘后面0的数量
    51nod 2122 分解质因数
    javascript中的setter和getter
  • 原文地址:https://www.cnblogs.com/taiyonghai/p/6524910.html
Copyright © 2011-2022 走看看