浅谈C#4.0协变性与逆变性

来源:互联网 发布:怎么用淘宝优惠券赚钱 编辑:程序博客网 时间:2024/04/30 11:46

 

浅谈C#4.0协变性与逆变性  

2012-11-17 22:30:05|  分类:.net编程|  标签:|举报|字号 订阅

本文重点不在于阐述out(协变性)和 in(逆变性)的使用,而是针对它们为什么要这样设计,这样做有什么好处或是怎么运作来阐述的。在理解其为何这么做的时候,我通过一些假设,并且对这些假 设进行验证,这样理解起来比较清晰。很多时候,我们知道要这样做,但却不知道为什么要这样做,导致对其只是一个机械式的理解。我们不仅要做到知其然,而且 要知其所以为然(个人观点)

一、 协变性和逆变性是什么?

协变性:派生程度较大类型分配(赋值)给派生程度较小类型。在泛型参数中使用out类型参数修饰符,例如:

代码1-1

1 IEnumerable<string> strs = new List<string>();2 IEnumerable<object> objs = strs;3 //其中类型string相对类型object的派生程度较大

逆变性:派生程度较小类型分配(赋值)给派生程度较大类型。在泛型参数中使用in类型参数修饰符,例如:

代码1-2

1 //SetObjValue的方法为public void SetObjeValue(object objValue){}2 Action<object> objActions = SetObjValue;3 //其中类型object相对类型string的派生程度较小
二、支持协变性和逆变性有哪些?

1.数组,从C#1.0数组就开始支持协变性和逆变性,但到了C#2.0及以上的版本均只支持协变性 

代码2-1

1 object[] objs = new string[] { };//协变:C# 1.0以上版本 2 string[] strs = (string[])new object[] { }; //逆变:C# 2.0及以上版本不支持,C#1.0支持
2.泛型委托中的协变性与逆变性(C#2.0及以上版本)
代码2-2
1 public string GetStrValue() { return ""; }2 public void SetStrValue(string str) { }3 public object GetObjValue() { return null; }4 public void SetObjValue(object obj) { }
代码2-3
1 //delegate void Action<in T > 此特性为C#2.0新添,无返回参数,可参考MSDN文档2 Action<string> m1 = SetStrValue;3 Action<string> m2 = SetObjValue;//逆变4 //delegate outResult Func<out outReuslt> 此特性为C#4.0新添 有返回参数 5 Func<string> m3 = GetStrValue;6 Func<object> m4 = GetStrValue;//协变
 3.泛型接口中协变性与逆变性(C#4.0版本)
代码2-4
1 IEnumerable<string> strs = new List<string>();2 IEnumerable<object> objs = strs;3 //其中IEnumerable的类型参数是用out修饰的,IEnumerable<out T> 支持协变性。4 //有关泛型接口的逆变性可参考IComparer<in T>接口 ,在这不再详举
三、协变性:为何out 修饰符修饰后只能返回值或属性的取值方法(输出),不能作为参数或属性的设值方法(输入)。

 大家先看看代码3-1这个例子, 这个例子在一定程度上说明了为何要设计out 类型参数修饰符。

代码3-1

 1     abstract class Person { } 2     class Employee : Person { } 3     class Customer : Person { } 4  5     class Program 6     { 7  8         public static Person[] M1(Person[] persons) 9         {10             persons[0] = new Customer(); //编译通过,但运行出错!11             return persons;12         }13         static void Main(string[] args)14         {15             Person[] persons = new[] { new Employee() };//数组协变性16             M1(persons);17         }18
在代码3-1,Employee类与Customer类派生自Person类,定义了Person 的数组,里面装的是一个Employee对象,当调用的M1的时候,会出现错误。原因:Person数组的数据项类型是 Employee,Customer类型与Employee类型不兼容。由此看来,数组虽然支持协变性,但却不能保证类型安全,而out正是为了泛型类型 之间在协变时转换,防止出现这种情况,从而保证类型的安全,因为out修饰后只能返回值而不能改变其值。

下面我们来看一个关于out 类型参数修饰符的使用。

代码3-2

 1     interface IMyInterface<T> 2     { 3         T GetPerson { get; set; } 4     } 5  6     interface IReadOnly<out T> 7     { 8         T GetPerson { get; }//若添加了set访问器,编译不通过。由于out 修饰后,只支持返回、输出,不支持改写和输入参数 9 10     }11     class Person { };12     class Employee : Person { };13     class Customer : Person { };14     class MyClass<T> : IReadOnly<T>//支持协变性15     {16         T _person;17         public T GetPerson18         {19             get { return _person; }20             set { _person = value; }21         }22         public MyClass(T person)23         {24             this._person = person;25         }26     }27     class Program28     {29         static void Main(string[] args)30         {31             MyClass<Employee> myClass = new MyClass<Employee>(new Employee());32             IReadOnly<Person> myInterface = myClass;//在这里IReadOnly<Person>的会将MyClass中的Employee都向上转换为基类Person33             //若去掉IReadOnly的out修饰符,则编译不通过。因为去掉out后,IReadOnly<Person>类型与MyClass<Employee>类型之间不支持协变34             //所以编译器会检测到这两者类型不兼容,从而编译出错35         }36     }
以上代码是out修饰符的一个例子,因为这不是阐述重点并且注释已经写得比较明白,在这不多解释。为了增加可读性和理解,在下面我增加了些伪代码。代码3-2是在上面示例第32行代码myClass内部转换的类似伪代码

代码3-3

 1     class MyClass<Employee> : IReadOnly<Person> //此代码为伪代码,只用于理解 2     { 3         Employee _person; 4         public Person GetPerson       5         { 6             get { return _person; } 7         } 8         public MyClass(Employee person) 9         {10             this._person = person;11         }12     }
为了进一步解释out类型参数修饰符修饰后为什么只能只读(作为返回值)而不能改写,现在我们来开始假设,如果可以改写,在代码3-3第6行后插入set{_person = value; }
那么会产生这样的代码
 代码3-4
 1 class MyClass<Employee> : IReadOnly<Person> 2     { 3         Employee _person; 4         public Person GetPerson 5         { 6             get { return _person; } 7             set { _person = value; } 8         } 9         public MyClass(Employee person)10         {11             this._person = person;12         }13     }
代码3-5
1         static void Main(string[] args)2         {3             MyClass<Employee> myClass = new MyClass<Employee>(new Employee());4             IReadOnly<Person> myInterface = myClass;5             myInterface.GetPerson = new Customer(); //这里出错了6         }
当我们假设了可以set后,在代码3-5的第5行中,由于myInterface.GetPerson的类型是Person,所以可以将Customer 赋给myInterface.GetPerson, 但再看看代码3-4,_person的类型却是Employee,将Customer类型赋给Employee类型,明显不可以,肯定会出错,从而我们的 假设也不攻而破了。
所以,为何out修饰后,只能作为返回值,而不能改变其值,是为了防止出现刚刚我们假设的情况,从而保证了泛型在类型转换时的类型安全。

 

四、逆变性:为何in修饰符修饰后只能作为参数或属性的设值方法(输入),不能作为返回值或属性的取值方法(输出)。

 在这也讨论下in修饰符修饰后的例子:

代码4-1

 1     interface IMyInterface<T> 2     { 3         T GetPerson { get; set; } 4     } 5  6     interface IWriteOnly<in T> 7     { 8         T GetPerson { set; }//若添加了get访问器,编译不通过。由于in 修饰后,只能作为参数或属性的设值方法,不能作为返回值 9     }10     class Person { };11     class Employee : Person { };12     class Customer : Person { };13     class MyClass<T> : IWriteOnly<T>//支持逆变性14     {15         T _person;16         public T GetPerson17         {18             set { _person = value; }19         }20         public MyClass(T person)21         {22             this._person = person;23         }24     }25     class Program26     {27         static void Main(string[] args)28         {29             MyClass<Person> myClass2 = new MyClass<Person>(new Employee());30             IWriteOnly<Customer> myInterface2 = myClass2;//Person 转为 Customer 为逆变31             //若去掉IWriteOnly的in修饰符,则编译不通过。由于去掉in后,IWriteOnly<Customer>类型与MyClass<Person>类型之间不支持逆变32             //所以编译器会检测到两者类型不兼容,从而编译出错33 34         }35    }
代码4-2是在上面示例第30行代码myClass2内部转换的类似伪代码

代码4-2 

 1     class MyClass<Person> : IWriteOnly<Customer>//伪代码,只供理解 2     { 3         Person _person; 4         public Customer GetPerson 5         { 6             //get { return _person; }//假设可以get,去掉注释 7             set { _person = value; } 8         } 9         public MyClass(Person person)10         {11             this._person = person;12         }13     }
  代码4-3
1         static void Main(string[] args)2         {3             MyClass<Person> myClass2 = new MyClass<Person>(new Employee());4             IWriteOnly<Customer> myInterface2 = myClass2;5             Customer customer = myInterface2.GetPerson; //这里会出错6 7         }
若我们假设可以get(返回值),去掉代码12第6行注释。在执行代码4-3中的第5行中 的Customer customer = myInterface2.GetPerson ,由于代码4-2中的_person引用的是Employee类型,在return _person的时候,无法将_person的引用类型Employee转为Customer类型,在这我们的假设就也不成立了。所以为何in修饰符修饰后不能作为返回值,其实也是为了保证类型安全。

 

五、总结:

在out修饰后(标记为协变),只能作为方法返回值或属性取值方法

在in 修饰后(标记为逆变),只能作为方法参数或属性的设值方法

另外,强调一下out 与 in 类型参数修饰符只能用在泛型接口和泛型委托,不能用在类中。
在上面是以属性来做例子,其实也可以把属性改为方法,也是一样的。(其实我们所说的属性本质就是方法,通过IL代码便可知道 ,参考《CLR Via C# 第三版》)

转自:http://www.soaspx.com/dotnet/csharp/csharp_20120403_8868.html
0 0