协变和逆变
转发:https://www.cnblogs.com/ninputer/archive/2008/11/22/generic_covariant.html
背景知识:协变和逆变
假设有这样两个类型:tsub是tparent的子类,显然tsub型引用是可以安全转换为tparent型引用的。如果一个泛型接口ifoo<t>,ifoo<tsub>可以转换为ifoo<tparent>的话,我们称这个过程为协变,而且说这个泛型接口支持对t的协变。而如果一个泛型接口ibar<t>,ibar<tparent>可以转换为t<tsub>的话,我们称这个过程为逆变(contravariant),而且说这个接口支持对t的反变。因此很好理解,如果一个可变性和子类到父类转换的方向一样,就称作协变;而如果和子类到父类的转换方向相反,就叫逆变性。
.net 4.0引入的泛型协变、逆变性
刚才我们讲解概念的时候已经用了泛型接口的协变和逆变,但在.net 4.0之前,c#不支持泛型的这种可变性。不过它们都支持委托参数类型的协变和逆变。由于委托参数类型的可变性理解起来抽象度较高,所以我们这里不准备讨论。已经完全能够理解这些概念的读者自己想必能够自己去理解委托参数类型的可变性。在.net 4.0之前为什么不允许ifoo<t>进行协变或逆变呢?因为对接口来讲,t这个类型参数既可以用于方法参数,也可以用于方法返回值。设想这样的接口
1 interface ifoo<t> 2 { 3 void method1(t param); 4 t method2(); 5 }
如果我们允许协变,从ifoo<tsub>到ifoo<tparent>转换,那么ifoo.method1(tsub)就会变成ifoo.method1(tparent)。我们都知道tparent是不能安全转换成tsub的,所以method1这个方法就会变得不安全。同样,如果我们允许反变ifoo<tparent>到ifoo<tsub>,则tparent ifoo.method2()方法就会变成tsub ifoo.method2(),原本返回的tparent引用未必能够转换成tsub的引用,method2的调用将是不安全的。有此可见,在没有额外机制的限制下,接口进行协变或反变都是类型不安全的。.net 4.0改进了什么呢?它允许在类型参数的声明时增加一个额外的描述,以确定这个类型参数的使用范围。我们看到,如果一个类型参数仅仅能用于函数的返回值,那么这个类型参数就对协变相容。而相反,一个类型参数如果仅能用于方法参数,那么这个类型参数就对逆变相容。如下所示:
1 interface ico<out t> 2 { 3 t method(); 4 } 5 6 interface icontra<in t> 7 { 8 void method(t param); 9 }
在.net framework中,许多接口都仅仅将类型参数用于参数或返回值。为了使用方便,在.net framework 4.0里这些接口将重新声明为允许协变或逆变的版本。例如icomparable<t>就可以重新声明成icomparable<in t>,而ienumerable<t>则可以重新声明为ienumerable<out t>。不过某些接口ilist<t>是不能声明为in或out的,因此也就无法支持协变或逆变。可以看到c#4用out来描述仅能作为返回值的类型参数,用in来描述仅能作为方法参数的类型参数。一个接口可以带多个类型参数,这些参数可以既有in也有out,因此我们不能简单地说一个接口支持协变还是逆变,只能说一个接口对某个具体的类型参数支持协变或逆变。比如若有ibar<in t1, out t2>这样的接口,则它对t1支持反变而对t2支持协变。举个例子来说,ibar<object, string>能够转换成ibar<string, object>,这里既有协变又有逆变。
下面提起几个泛型协变和逆变容易忽略的注意事项:
1)仅有泛型接口和泛型委托支持对类型参数的可变性,泛型类或泛型方法是不支持的。
2)值类型不参与协变或逆变,ifoo<int>永远无法变成ifoo<object>,不管有无声明out。因为.net泛型,每个值类型会生成专属的封闭构造类型,与引用类型版本不兼容。
3)声明属性时要注意,可读写的属性会将类型同时用于参数和返回值。因此只有只读属性才允许使用out类型参数,只写属性能够使用in参数。
协变和逆变的相互作用
这是一个相当有趣的话题,我们先来看一个例子:
1 interface ifoo<in t> 2 { 3 } 4 5 interface ibar<in t> 6 { 7 void test(ifoo<t> foo); //对吗? 8 }
1 interface ifoo<in t> 2 { 3 } 4 5 interface ibar<out t> 6 { 7 void test(ifoo<t> foo); 8 }
你能看出上述代码有什么问题吗?我声明了in t,然后将他用于方法的参数了,一切正常。但出乎你意料的是,这段代码是无法编译通过的!反而是这样的代码通过了编译:
什么?明明是out参数,我们却要将其用于方法的参数才合法?初看起来的确会有一些惊奇。我们需要费一些周折来理解这个问题。现在我们考虑ibar<string>,它应该能够协变成ibar<object>,因为string是object的子类。因此ibar.test(ifoo<string>)也就协变成了ibar.test(ifoo<object>)。当我们调用这个协变后方法时,将会传入一个ifoo<object>作为参数。想一想,这个方法是从ibar.test(ifoo<string>)协变来的,所以参数ifoo<object>必须能够变成ifoo<string>才能满足原函数的需要。这里对ifoo<object>的要求是它能够逆变成ifoo<string>!而不是协变。也就是说,如果一个接口需要对t协变,那么这个接口所有方法的参数类型必须支持对t的逆变。同理我们也可以看出,如果接口要支持对t逆变,那么接口中方法的参数类型都必须支持对t协变才行。这就是方法参数的协变-逆变互换原则。所以,我们并不能简单地说out参数只能用于返回值,它确实只能直接用于声明返回值类型,但是只要一个支持逆变的类型协助,out类型参数就也可以用于参数类型!换句话说,in参数除了直接声明方法参数之外,也仅能借助支持协变的类型才能用于方法参数,仅支持对t逆变的类型作为方法参数也是不允许的。要想深刻理解这一概念,第一次看可能会有点绕,建议有条件的情况下多进行一些实验。
刚才提到了方法参数上协变和逆变的相互影响。那么方法的返回值会不会有同样的问题呢?我们看如下代码:
1 interface ifooco<out t> 2 { 3 } 4 5 interface ifoocontra<in t> 6 { 7 } 8 9 interface ibar<out t1, in t2> 10 { 11 ifooco<t1> test1(); 12 ifoocontra<t2> test2(); 13 }
我们看到和刚刚正好相反,如果一个接口需要对t进行协变或逆变,那么这个接口所有方法的返回值类型必须支持对t同样方向的协变或逆变。这就是方法返回值的协变-逆变一致原则。也就是说,即使in参数也可以用于方法的返回值类型,只要借助一个可以逆变的类型作为桥梁即可。