泛型约束
与模板不同,泛型定义支持形式约束语法,这些语法用于描述可以合法地绑定的类型参数。在我详细介绍约束功能之前,我们简短地考虑一下为什么泛型选择了提供约束功能,而模板选择了不提供这个功能。我相信,最主要的原因是两种机制的绑定时间之间差异。
模板在编译的过程中绑定,因此无效的类型会让程序停止编译。用户必须立即解决这个问题或者把它重新处理成非模板编程方案。执行程序的完整性不存在风险。
另一方面,泛型在运行时绑定,在这个时候才发现用户指定的类型无效就已经太迟了。因此通用语言结构(CLI)需要一些静态(也就是编译时)机制来确保在运行时只会绑定有效的类型。与泛型相关的约束列表是编译时过滤器,也就是说,如果违反的时候,会阻止程序的建立。
我们来看一个例子。图5显示了用泛型实现的容器类。它的搜索方法假设类型参数衍生自Icomparable,因此它实现了该接口的CompareTo方法的一个实例。请注意,容器的大小是在构造函数中由用户提供的,而不是作为第二个、非类型参数提供的。你应该记得泛型不支持非类型参数的。
代码5:作为泛型实现的容器
generic <class elemType> public ref class Container { array<elemType> ^m_buf; int next; int size; public: bool search( elemType et ) { for each ( elemType e in m_buf ) if ( et->CompareTo( e )) return true; return false; }
Container( int sz ) { m_buf = gcnew array<elemType>(size = sz); next = 0; }
// add() 和 get() 是相同的 ...
}; |
该泛型类的实现在编译的时候失败了,遇到了如下所示的致命的编译诊断信息:
error C2039: 'CompareTo' : is not a member of 'System::Object' |
你也许有点糊涂了,这是怎么回事?没有人认为它是System::Object的成员啊。但是,在这种情况下你就错了。在默认情况下,泛型参数执行最严格的可能的约束:它把自己的所有类型约束为Object类型。这个约束条件是对的,因为只允许CLI类型绑定到泛型上,当然,所有的CLI类型都多多少少地衍生自Object。因此在默认情况下,作为泛型的作者,你的操作非常安全,但是可以使用的操作也是有限的。
你可能会想,好吧,我减小灵活性,避免编译器错误,用等于操作符代替CompareTo方法,但是它却引起了更严重的错误:
error C2676: binary '==' : 'elemType' does not define this operator or a conversion to a type acceptable to the predefined operator |
同样,发生的情况是,每个类型参数开始的时候都被Object的四个公共的方法包围着:ToString、GetType、GetHashCode和Equals。其效果是,这种在单独的类型参数上列出约束条件的工作表现了对初始的强硬约束条件的逐步放松。换句话说,作为泛型的作者,你的任务是按照泛型约束列表的约定,采用可以验证的方式来扩展那些允许的操作。我们来看看如何实现这样的事务。
我们用约束子句来引用约束列表,使用非保留字"where"实现。它被放置在参数列表和类型声明之间。实际的约束包含一个或多个接口类型和/或一个类类型的名称。这些约束显示了参数类型希望实现的或者衍生出类型参数的基类。每种类型的公共操作集合都被添加到可用的操作中,供类型参数使用。因此,为了让你的elemType参数调用CompareTo,你必须添加与Icomparable接口关联的约束子句,如下所示:
generic <class elemType> where elemType : IComparable public ref class Container { // 类的主体没有改变 ... }; |
这个约束子句扩展了允许elemType实例调用的操作集合,它是隐含的Object约束和显式的Icomparable约束的公共操作的结合体。该泛型定义现在可以编译和使用了。当你指定一个实际的类型参数的时候(如下面的代码所示),编译器将验证实际的类型参数是否与将要绑定的类型参数的约束相匹配:
int main() { // 正确的:String和int实现了IComparable Container<String^> ^sc; Container<int> ^ic;
//错误的:StringBuilder没有实现IComparable Container<StringBuilder^> ^sbc; } |
编译器会提示某些违反了规则的信息,例如sbc的定义。但是泛型的实际的绑定和构造已经由运行时完成了。
接着,它会同时验证泛型在定义点(编译器处理你的实现的时候)和构造点(编译器根据相关的约束条件检查类型参数的时候)是否违反了约束。无论在那个点失败都会出现编译时错误。
约束子句可以每个类型参数包含一个条目。条目的次序不一定跟参数列表的次序相同。某个参数的多个约束需要使用逗号分开。约束在与每个参数相关的列表中必须唯一,但是可以出现在多个约束列表中。例如:
generic <class T1, class T2, class T3> where T1 : IComparable, ICloneable, Image where T2 : IComparable, ICloneable, Image where T3 : ISerializable, CompositeImage public ref class Compositor { // ... }; |
在上面的例子中,出现了三个约束子句,同时指定了接口类型和一个类类型(在每个列表的末尾)。这些约束是有额外的意义的,即类型参数必须符合所有列出的约束,而不是符合它的某个子集。我的同事Jon Wray指出,由于你是作为泛型的作者来扩展操作集合的,因此如果放松了约束条件,那么该泛型的用户在选择类型参数的时候就得增加更多的约束。
T1、T2和T3子句可以按照其它的次序放置。但是,不允许跨越两个或多个子句指定某个类型参数的约束列表。例如,下面的代码就会出现违反语法错误:
generic <class T1, class T2, class T3> // 错误的:同一个参数不允许有两个条目 where T1 : IComparable, ICloneable where T1 : Image public ref class Compositor { // ... }; |
类约束类型必须是未密封的(unsealed)参考类(数值类和密封类都是不允许的,因为它们不允许继承)。有四个System名字空间类是禁止出现在约束子句中的,它们分别是:System::Array、System::Delegate、 System::Enum和System::ValueType。由于CLI只支持单继承(single inheritance),约束子句只支持一个类类型的包含。约束类型至少要像泛型或函数那样容易访问。例如,你不能声明一个公共泛型并列出一个或多个内部可视的约束。
任何类型参数都可以绑定到一个约束类型。下面是一个简单的例子:
generic <class T1, class T2> where T1 : IComparable<T1> where T2 : IComparable<T2> public ref class Compositor { // ... }; |
约束是不能继承的。例如,如果我从Compositor继承得到下面的类,Compositor的T1和T2上的Icomparable约束不会应用在 BlackWhite_Compositor类的同名参数上:
generic <class T1, class T2> public ref class BlackWhite_Compositor : Compositor { // ... }; |
当这些参数与基类一起使用的时候,这就有几分设计方面的便利了。为了保证Compositor的完整性,BlackWhite_Compositor必须把Compositor约束传播给那些传递到Compositor子对象的所有参数。例如,正确的声明如下所示:
generic <class T1, class T2> where T1 : IComparable<T1> where T2 : IComparable<T2> public ref class BlackWhite_Compositor : Compositor { // ... }; |
包装 你已经看到了,在C++/CLI下面,你可以选择CLR泛型或C++模板。现在你所拥有的知识已经可以根据特定的需求作出明智的选择了。在两种机制下,超越元素的存储和检索功能的参数化类型都包含了每种类型参数必须支持操作的假设。
使用模板的时候,这些假设都是隐含的。这给模板的作者带来了很大的好处,他们对于能够实现什么样的功能有很大的自由度。但是,这对于模板的使用者是不利的,他们经常面对某些可能的类型参数上的没有正式文档记载的约束集合。违反这些约束集合就会导致编译时错误,因此它对于运行时的完整性不是威胁,模板类的使用可以阻止失败出现。这种机制的设计偏好倾向于实现者。
使用泛型的时候,这些假设都被明显化了,并与约束子句中列举的基本类型集合相关联。这对泛型的使用者是有利的,并且保证传递给运行时用于类型构造的任何泛型都是正确的。据我看来,它在设计的自由度上有一些约束,并且使某些模板设计习惯稍微难以受到支持。对这些形式约束的违反,无论使在定义点还是在用户指定类型参数的时候,都会导致编译时错误。这种机制的设计偏好倾向于消费者。
查看本文来源