12.8 可验证性和约束
where关键字
编译器和CLR支持称为约束的机制,可通过它使泛型变得真正有用。
约束的作用限制能指定成泛型实参的类型数量,通过限制类型的数量,可以对那些类型执行更多操作:
public static T Min<T>(T o1,T o2) where T:IComparable<T>{ if(o1.ComparaTo(o2)<0) return o1; return o2; }
C#的where关键字告诉编译器,为T制定的任何类型都必须实现同类型T的泛型IComparable接口。
有了这个约束就可以在方法中调用CompareTo,因为已知IComparable<T>接口定义了CompareTo.
现在当代码引用泛型类型或方法时,编译器要负责保证类型实参符合指定的约束,例如:
private static void CallMin(){ Object o1="A",o2="B"; Object oMin=Min<Object>(o1,o2);//Error CS0311 }
编译器便会报错,以为内System.Object没有实现IComparable<Object>接口,事实上Object没有实现任何接口。
约束不能用于重载
约束可应用于泛型类型的类型参数,也可应用于泛型方法的类型参数。
CLR不允许基于类型参数名称或约束来进行重载:只能基于类型参数个数对类型或方法进行重载:
//可定义的类型 internal sealed class AType{} internal sealed class AType<T>{} internal sealed class AType<T1,T2>{} //错误,与没有约束的AType<T>冲突 internal sealed class AType<T> where T:IComparable<T>{} //错误,与AType<T1,T2>冲突 internal sealed class AType<T3,T4>{} internal sealed class AnotherType{ //可定义以下方法 private static void M(){} private static void M<T>(){} private static void M<T1,T2>(){} //错误,与没有约束的M<T>冲突 private static void M<T>() where T:IComparable<T>{} //错误,与M<T1,T2>冲突 private static void M<T3,T4)(){} }
虚方法的约束
重写虚泛型方法时,重写的方法必须指定相同数量的类型参数,而且这些类型参数会继承在基类方法上指定的约束。
事实上根本不允许为重写方法的类型参数指定任何约束,但类型参数的名称是可以改变的。
实现接口方法时方法必须指定与接口方法等量的类型参数,这些类型参数将继承由接口方法指定的约束:
internal class base{ public virtual void M<T1,T2>() where T1:struct where T2:class{} } internal sealed class Derived:Base{ public override void M<T3,T4>() where T3:EventArgs where T4:class{}//错误 }
编译以上代码,会报错:重写和显式接口实现方法的约束是从基方法继承的,因此不能直接指定这些约束。
从Derived类的M<T3,T4>方法中移除两个where子句,代码就能正常编译了。
注意,类型参数的名称可以更改,比如T1改成T3;但约束不能更改甚至不能指定。
12.8.1 主要约束
类型参数可以指定零个或者一个主要约束,主要约束可以是代表非密封类的一个引用类型。
不能约束以下特殊引用类型:System.Object、Array、Delegate、MulticastDelegate、ValueType、Enum、Void。
引用类型约束
指定引用类型约束时,相当于向编译器承诺:一个指定的类型实参要么是与约束类型相同的类型,要么是从约束类型派生的类型:
internal sealed class PrimaryConstraintOfStream<T> where T:Stream{ public void M(T stream){ stream.Close();//正确 } }
类型参数T设置了主要约束Stream。这就告诉编译器使用PrimaryConstraintOfStream的代码在指定类型实参时,必须指定Stream或者从其中派生的类型比如FileStream。
如果类型参数没有指定主要约束,就默认为System.Object,但若果在源代码中显式指定Object,就会报错。
特殊的主要约束
有两个特殊的主要约束:struct和class。其中class约束向编译器承诺类型参数是引用类型。
任何类类型、接口类型、委托类型或者数组类型都满足这个约束:
internal sealed class PrimaryConstraintOfClass<T> where T:class{ public void M(){ T temp=null;//允许,因为T肯定是引用类型 } }
值类型约束
struct约束向编译器承诺类型实参是值类型。包括枚举在内的任何值类型都满足这个约束。
但编译器和CLR将任何System.Nullable<T>值类型视为特殊类型,不满足struct约束。
原因是Nullable<T>类型将它的类型参数约束为struct,而CLR希望禁止向Nullable<Nullable>>这样的递归类型。可空类型在第十九章讨论。
以下是示例使用struct约束他的类型参数:
internal sealed class PrimaryConstraintOfStruct<T> where T:struct{ public void T Factory(){ return new T();//允许,因为所有值类型都隐藏有一个公共无参构造器 } }
例子中的new T()是合法的,因为T已知是值类型,而所有的值类型都隐式有一个公共无参构造器。
如果是class,上述代码无法编译,因为有的引用类型没有公共无参构造器。
12.8.2 次要约束
接口约束
类型参数可以指定零个或多个次要约束,次要约束代表接口类型。这种约束向编译器承诺类型实参实现了接口。
由于能指定多个接口约束,所以类型实参必须实现了所有接口约束。第十三章详细讨论。
类型参数约束
还有一种次要约束称为类型参数约束,有时也称为裸类型约束。这种约束用的比接口约束少得多。
它允许一个泛型类型或方法规定:指定的类型实参要么就是约束的类型,要么就是约束的类型的派生类。
一个类型参数可以指定零个或者多个类型参数约束,下面这个泛型方法演示了如何使用类型参数约束:
private static List<TBase> ConvertIList<T,TBase>(IList<T> list) where T:TBase{ List<TBase> baseList=new List<TBase>(list.Count); for(Int32 index=0;index<list.Count;index++){ baseList.Add(list[index]); } return baseList; }
ConvertIList方法指定了两个类型参数,其中T参数由TBase类型参数约束。
意味着不管为T指定什么类型实参,都必须兼容于TBase指定的类型实参。
下面这个方法演示了对ConvertIList的合法调用和非法调用:
//构造并初始化一个List<String>,他实现了Ilist<String> IList<String ls=new List<String>(); ls.Add("A String");
//1.将IList<String>转换成一个IList<Object> IList<Object> lo=ConvertIList<String,Object>(ls);
//2.将IList<String>转换成一个IList<IComparable> IList<IComparable> lc=ConvertIList<String,IComparable>(ls);
//3.将IList<String>转换成一个IList<Icomparable<String>> Ilst<IComparable<String>> lcs=ConvertIList<String,Icomparable<String>>(ls);
//4.将IList<String>转换成一个IList<String> IList<String> ls2=ConvertIList<String,String>(ls);
//5.将IList<String>转换成一个IList<Exception> IList<Exception> ls2=ConvertIList<String,Exception>(ls);//错误
12.8.3 构造器约束
类型参数可以指定零个或者一个构造器约束,它向编译器承诺类型实参是实现了公共无参构造器的非抽象类型。
注意如果同时使用构造器约束和struct约束,编译器会认为这是一个错误,因为这是多余的。
所有值类型都隐式提供了公共无参构造器,以下实例类使用构造器约束来约束他的类型参数:
internal sealed class PrimaryConstraintOfStruct<T> where T:new(){ public void T Factory(){ //允许,因为所有值类型都隐藏有一个公共无参构造器 //而如果指定的是引用类型约束也要求它提供公共无参构造器 return new T(); } }
这个例子中的new T()是合法的,因为已知T是拥有公共无参构造器的类型。对所有值类型来说这一点肯定成立