小台的IT备忘录  
脑子越来越不好用,只能依靠烂笔头了~

协变与逆变是.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也会有相应的提示,这样做也可以增加【函数传入参数值、【函数返回值】的扩展性,何乐而不为呢~

posted on 2017-03-09 11:44  taiyonghai  阅读(924)  评论(0编辑  收藏  举报