浅谈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及以上版本)
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版本)
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 GetPerson 18 { 19 get { return _person; } 20 set { _person = value; } 21 } 22 public MyClass(T person) 23 { 24 this._person = person; 25 } 26 } 27 class Program 28 { 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都向上转换为基类Person 33 //若去掉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 GetPerson 17 { 18 set { _person = value; } 19 } 20 public MyClass(T person) 21 { 22 this._person = person; 23 } 24 } 25 class Program 26 { 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# 第三版》)