泛型接口
定义
先来看一个简单的例子:
public class Sharp
{}
public class Rectangle:Sharp
{}
上面定义了两个简单的类,一个是图形类,一个是矩形类;他们之间有简单的继承关系,正确的写法:
Sharp sharp=new Rectangle();
就是说“子类引用可以直接转化成父类引用”,或者说Rectange类和Sharp类之间存在一种安全的隐式转换。
那问题就来了,既然Rectange类和Sharp类之间存在一种安全的隐式转换,那数组Rectange[]和Sharp[]之间是否也存在这种安全的隐式转换呢?
这就牵扯到了将原本类型上存在的类型转换映射到他们的数组类型上的能力,这种能力就称为“可变性(Variance)”。在.NET中,唯一允许可变性的类型转换就是由继承关系带来的“子类引用->父类引用”转换。也就是上面例子所满足的写法。
下例:
Sharp [] sharps=new Rectangle[3];
这是可以的,这说明Rectange[]和Sharp[]之间存在安全的隐式转换。
像这种与原始类型转换方向相同的可变性就称作协变(covariant)
下例:
Rectangle [] rectanges=new Sharp[3];
这是不行,编译不通过,即数组所对应的单一元素的父类引用不可以安全的转化为子类引用。数组也就自然不能依赖这种可变性,达到协变的目的。
所以与协变中子类引用转化为父类引用相反,将父类引用转化为子类引用的就称之为抗变。
即:一个可变性和子类到父类转换的方向一样,就称作协变;而如果和子类到父类的转换方向相反,就叫抗变!
当然可变性远远不只是针对映射到数组的能力,也有映射其它集合的能力如List<T>.
到这里,很多人就会问了,说了这么多,那到底这个协变或者抗变有什么实际利用价值呢?
其价值就在于,在.net 4.0之前可以这么写:
Sharp sharp=new Rectangle();
但是不能这么写:
IEnumerable<Sharp> sharps=newList<Rectangle>();
4.0之后就允许了,因为IEnumerable<T>被声明成如下形式:
public interface IEnumerable<out T>:IEnumerable.
数组是不支持抗变的.在.NET4.0之后,支持协变和抗变的两种类型:泛型接口和泛型委托.
先来看泛型接口中的协变和抗变
定义一个泛型接口:
public interface ICovariant<T>
{}
让上面的两个类各自继承一下该接口:
public class Sharp:ICovariant<Sharp>
{}
public class Rectangle: Sharp,ICovariant<Rectangle>
{}
编写测试代码:
static void Main(string[] args)
{
ICovariant<Sharp> isharp = new Sharp();
ICovariant<Rectangle> irect = new Rectangle();
isharp = irect;
}
发现编译不通过,因为无法将ICovariant<Rectange>隐式转化为ICovariant<Sharp>!
修改接口为:
public interface ICovariant<out T>
{ }
编译顺利通过。这里我为泛型接口的类型参数增加了一个修饰符out,它表示这个泛型接口支持对类型T的协变。
即:如果一个泛型接口IFoo<T>,IFoo<TSub>可以转换为IFoo<TParent>的话,我们称这个过程为协变,而且说“这个泛型接口支持对T的协变”。
那我如果反过来呢,考虑如下代码:
ICovariant<Sharp> isharp = new Sharp();
ICovariant<Rectangle> irect = new Rectangle();
//isharp = irect;
irect = isharp;
编译错误,原因是无法将ICovariant<Sharp>隐式转换为ICovariant<Rectangle>!
修改接口为:
public interface ICovariant<in T>
{ }
编译顺利通过,这里我将泛型接口的类型参数T修饰符修改成in,它表示这个泛型接口支持对类型参数T的抗变。
即:如果有一个泛型接口IFoo<T>,IFoo<TParent>可以转换为IFoo<TSub>的话,我们就称这个过程为抗变,而且说”这个泛型接口支持对T的抗变”.
泛型接口并不单单只有一个参数,所以我们不能简单地说一个接口支持协变还是抗变,只能说一个接口对某个具体的类型参数支持协变或抗变,如ICovariant<out T1,in T2>说明该接口对类型参数T1支持协变,对T2支持抗变。
举个例子就是:ICovariant<Rectange,Sharp>能够转化成ICovariant<Sharp,Rectange>,这里既有协变也有抗变。
以上都是接口并没有属性或方法的情形,接下来给接口添加一些方法:
public interface ICovariant<in T>
{
T Method1();
void Method2(T parm);
}
只是单纯的定义一个接口,却发现编译不通过.而且无论用in还是out都不行,原因是,我把仅有的一个类型参数T即用作了函数的返回值,也用作了函数的参数类型.
(1) 当我用out修饰时,即允许接口对类型参数T协变,也就是满足从ICovariant<Rectangle>到ICovariant<Sharp>的转换,Method1返回值Rectangle到Sharp转换没有任何问题:
ICovariant<Rectangle> irect = new Rectangle();
ICovariant<Sharp> isharp = new Sharp();
isharp = irect;
Sharp sharp=isharp.Method1();
(2) 如果使用in关键字修饰时,允许接口对类型参数T抗变,也就是满足从ICovariant<Sharp>到ICovariant<Rectange>转换:
ICovariant<Rectangle> irect = new Rectangle();
ICovariant<Sharp> isharp = new Sharp();
irect = isharp;
irect.Method2(new Rectangle());
Method2(Sharp)会去替换Method2(Rectange),所以上面的最后一句代码无论以Rectange类型还是Sharp类型为参数都没有任何问题.
综上:在没有额外机制的限制下,接口进行协变或抗变都是类型不安全的。.NET 4.0有了改进,它允许在类型参数的声明时增加一个额外的描述,以确定这个类型参数的使用范围,这个额外的描述即in,out修饰符,它们俩的用法如下:
如果一个类型参数仅仅能用于函数的返回值,那么这个类型参数就对协变相容,用out修饰。而相反,一个类型参数如果仅能用于方法参数,那么这个类型参数就对抗变相容,用in修饰。
所以可以将上面的接口拆成两个接口即可:
public interface ICovariant1<out T>
{
T Method1();
}
public interface ICovariant2<in T>
{
void Method2(T parm);
}
.net中很多接口都仅将参数用于函数返回类型或函数参数类型,如:
public interface IComparable<in T>
public interface IEnumerable<out T>:IEnumerable
几个重要的注意点:
1.仅有泛型接口和泛型委托支持对类型参数的可变性,泛型类或泛型方法是不支持的。
2.值类型不参与协变或抗变,IFoo<int>永远无法协变成IFoo<object>,不管有无声明out。因为.NET泛型,每个值类型会生成专属的封闭构造类型,与引用类型版本不兼容。
3.声明属性时要注意,可读写的属性会将类型同时用于参数和返回值。因此只有只读属性才允许使用out类型参数,只写属性能够使用in参数。
接下来将接口代码改成:
public interface ICovariant<out T>
{
T Method1();
void Method3(IContravariant<T> param);
}
public interface IContravariant<in T>
{
void Method2(T param);
}
我们需要费一些周折来理解这个问题。现在我们考虑ICovariant<Rectange>,它应该能够协变成ICovariant<Sharp>,因为Rectange是Sharp的子类。因此Method3(Rectange)也就协变成了Method3(Sharp)。当我们调用这个协变,Method3(Sharp)必须能够安全变成Method3(Rectange)才能满足原函数的需要(具体原因上面已经示例过了)。这里对Method3的参数类型要求是Sharp能够抗变成Rectange!也就是说,如果一个接口需要对类型参数T协变,那么这个接口所有方法的参数类型必须支持对类型参数T的抗变(如果T有作为某些方法的参数类型)。
同理我们也可以看出,如果接口要支持对T抗变,那么接口中方法的参数类型都必须支持对T协变才行。这就是方法参数的协变-抗变互换原则。所以,我们并不能简单地说out参数只能用于方法返回类型参数,它确实只能直接用于声明返回值类型,但是只要一个支持抗变的类型协助,out类型参数就也可以用于参数类型!(即上面的例子),换句话说,in除了直接声明方法参数类型支持抗变之外,也仅能借助支持协变的类型才能用于方法参数,仅支持对T抗变的类型作为方法参数类型也是不允许的。
既然方法类型参数协变和抗变有上面的互换影响。那么方法的返回值类型会不会有同样的问题呢?
将接口修改为:
public interface IContravariant<in T>
{
}
public interface ICovariant<out T>
{
}
public interface ITest<out T1, in T2>
{
ICovariant<T1> test1();
IContravariant<T2> test2();
}
我们看到和刚刚正好相反,如果一个接口需要对类型参数T进行协变或抗变,那么这个接口所有方法的返回值类型必须支持对T同样方向的协变或抗变(如果有某些方法的返回值是T类型)。这就是方法返回值的协变-抗变一致原则。也就是说,即使in参数也可以用于方法的返回值类型,只要借助一个可以抗变的类型作为桥梁即可。