泛型(六)可验证性和约束
public static T Min<T o1,T o2> where T:IComparable<T>{ if(o1.CompareTo(02)<0) return o1; return o2; }
C#的关键字where告诉编译器,为T指定的任何类型都必须实现同类型(T)的泛型IComparable接口,有了这个约束,就可以在方法中调用CompareTo,因为已知IComparable<T>接口定义了CompareTo
现在,当代码引用一个泛型类型或者方法时,编译器要负责保证类型实参符合指定的约束,例如,假如编译以下代码:
public static void CallMin(){ object o1="jeff",o2="richer"; object oMin=Min<object>(o1,o2); }
编译器报错,因为System.Object没有实现IComparable<Object>接口,事实上,System.Object没有实现任何接口
更深一下研究约束,
1.约束可以应用于一个泛型类型的类型参数
2.也可以应用于一个泛型方法的类型参数(像Min方法那样)
3.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{ } }
error cs0460:重写和显示接口实现方法的约束是从基方法继承的,因此不能直接指定这些约束
如过从Derived类的M<T3,T4>方法中移除两个where子句,代码就能编译了,注意,类型参数的名称可以更改,比如将T1改为T2,T3改为T4,但约束不能更改(甚至不能指定)
主要约束
类型参数可以指定零个或一个主要约束,主要约束可以是一个引用类型,它标识了一个没有密封的类,不能指定以下特殊引用类型
System.Object
System.Array
System.Delegate
System.MulticastDelegate
System.ValueType
System.Enum
System.Void
指定一个引用类型约束时,相当月向编译器承诺:一个指定的类型参数要么是月约束类型相同的类型,要么是从约束类型派生的一个类型
例如下泛型类:
internal sealed class PrimaryConstraintOfStream<T> where T:Stream{ public void M(T stream){ stream.close(); //正确 } }
在这个类定义中,类型参数T设置了主要约束Stream,这就告诉编译器,使用PrimaryConstraintOfStream的代码在指定类型实参时,必须指定Sream或者从Stream派生一个类型(FileStream)。如果一个类型参数没有指定主要约束,就默认为System.Object。然而,如果源代码中显示指定System.Object,C#会报告一个错误:error cs0702:约束不能是特殊类“Object”
有两个特殊的主要约束:class和struct
1.class约束向编译器承诺一个指定的类型实参是引用类型。任何类类型,接口类型,委托类型或这是数组类型都满足这个约束。例如以下泛型:
internal sealed class PrimaryConstainOfClass<T> where T: class{ public void M(){ T temp=null //允许,因为T肯定是一个引用类型 } }
2.struct约束向编译器承诺一个指定的类型实参是值类型。包括枚举在内的任何值类型都满足这个约束。然而,编译器和CLR将任何System.Nullable<T>值类型都视为特殊类型,而且可空类型不满足这个struct约束,原因是Nullable<T>类型将他的类型实参约束为struct,而CLR希望禁止像Nullable<Nullable<T>>这样的一个递归类型。
例如:
internal sealed class PrimaryConstraintOfStruct<T> where T: struct{
//允许。因为所有值类型都隐式有一个公共无参构造器
return new T();
}
次要约束
一个类型参数可以指定零个或者多个次要约束,次要约束代表的是一个接口类型。指定一个接口类型约束时,是向编译器承诺一个指定的类型实参是实现了接口的一个类型。由于能指定多个接口约束,所以为类型实参指定的类型必须实现了所有接口约束。
还有一种约束成为类型参数约束,有时也称为裸类型约束。这种约束用的比接口约束少得多。它允许一个泛型类或者方法规定:在指定的类型实参之间,必须存在一个关系,一个类型参数可以指定零个或多个类型参数约束。例如
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指定的类型实参。
构造器约束
一个类型参数可以指定零个或者一个构造器约束。指定构造器约束相当于向编译器承诺:一个指定的类型实参是实现了公共无参构造器的一个非抽象类型。注意,如果同时指定了构造器的约束和struct约束,C#编译器会认为这是一个错误,因为这个是多余的;所有值类型都隐式提供了一个公共无参构造器,例如
internal sealed class ConstructorConstraint<T> where T: new (){ public static T Factory(){ //允许,因为所有值类型都隐式有一个公共无参构造器 //而约束要求指定的任何引用类型也要有一个公共无参构造器 return new T(); } }
其他可验证性问题
1.泛型类型变量的转型
将一个泛型类型的变量转型为另一个类型是非法的,除非将其转型为与一个约束兼容的类型
private static void CastingAGenericTypeVariable<T>(T obj){
Int32 x=(Int32)obj; //错误
String x=(String)obj; //错误
}
上述两行会造成编译报错,因为T可能是任意类型,无法保证转型成功,可以先转型为Object
private static void CastingAGenericTypeVariable<T>(T obj){
Int32 x=(Int32)(Object)obj; //无错误
String x=(String)(Object)obj; //无错误
}
编译无错,但会抛异常InvalidCastException
如果试图转型一个引用类型,还可以使用C#的as 操作符
private static void CastingAGenericTypeVariable<T>(T obj){
String s=obj as String;
}
2.将一个泛型类型变量设为默认值
将泛型类型变量值设为null是非法的,除非泛型类型约束成一个引用类型
private static void SettingAGenericTypeVariableToNull<T>(){
T temp=null; //error Cs043 无法将null转换为类型参数T,因为它可能是不可以为null的值类型。请考虑改用default(T)
T temp1=default(T); //正确 如果是T值类型,则默认为0
}
3.将一个泛型类型变量与null进行比较
无论泛型类型是否被约束,使用==或!=操作符将一个泛型类型变量与null进行比较都是合法的:
private static void CompareGenericTypeWithNull<T>(T obj)
{
if(obj==null) { /*对于值类型来说,永远都不会执行*/ }
}
4.两个泛型类型变量相互比较
如果泛型类型参数不是一个引用类型,对同一泛型类型的两个变量进行比较是非法的
private static oid CompareTwoGenericTypeVariables<T>(T o1,To2){
if(o1==o2){} //错误
}
T未进行约束,如果两个值类型变量相互比较是非法,除非值类型重载了==操作符。如果T被约束成class,上述代码能通过编译
不允许将类型参数约束成一个具体的值类型,因为值类型是隐式密封的,不可能存在派生,如果允许将类型参数约束成具体的值类型,那么泛型方法会被约束为只支持该具体的类型,这还不如写一个非泛型的方法呢!
5.泛型类型变量作为操作数使用
将操作符应用于泛型类型的操作数,会出现大量的问题,因为编译器在编译时无法确定类型。