Standard ECMA-372 1st Edition / December 2005
C++/CLI Language Specification
C++/CLI 语言详述
译者:Enzo Yang
8.9 值类型
值类型和ref类型的相似点是都可以包含有域和成员函数. 但是, 与ref类不同的是值类型不需要堆分配. 一个值类型的变量直接就包含了它的数据, 而ref类包含指向数据的句柄.
值类型对有值语义的小数据结构特别有用. 复数, 坐标系上的点, 或者是字典中的关键值对都是值类型的好例子. 这些数据结构的关键是它们只有很少的域, 他们不需要继承或者参考实体(referential identity),而且它们可以好方便地进行值复制(不是引用复制).
像int, double和bool这样的基本类型实际上都是值类型. 我们可以用值类型和重载操作符来实现新的”基本”类型.
value struct Point {
int x, y;
Point(int x, int y) {
this->x = x;
this->y = y;
}
};
8.10 接口
一个接口定义了一个合约(contract). 类要实现接口就必须根据合约来实现接口中所有的函数, 属性, 和事件.
delegate void EventHandler(Object^ sender, EventArgs^ e);
interface class IExample {
void F(int value);
property bool P { bool get(); }
property double default[int] {
double get(int);
void set(int, double);
}
event EventHandler^ E;
};
上面的接口含有一个函数F, 一个只读属性P, 一个默认索引属性和一个事件E, 所有这些都是隐式公有的.
接口的实现(implement)语法是.
interface class T1 { void F(); }; // F is implicitly virtual abstract
ref struct R1 : T1 { virtual void F() { /* implement T1::F */ } };
一个接口也可以实现一个或多个其它接口.
interface class IControl {
void Paint();
};
interface class ITextBox : IControl {
void SetText(String^ text) ;
};
interface class IListBox : IControl {
void SetItems(array<String^>^ items);
};
interface class IComboBox : ITextBox, IListBox { };
实现IComboBox的类也必须实现ITextBox, IListBox, 和 IControl接口
类可以实现不同个数的接口.
interface class IDataBound {
void Bind( Binder^ b);
};
public ref class EditBox : Control, IControl, IDataBound {
virtual void Paint() { … }
virtual void Bind( Binder ^ b) { … }
};
EditBox类继承自ref类Control实现了IControl和IDataBound.
在前面的例子中接口中的函数被隐式地实现了. C++/CLI提供了一种不让这些实现函数成为公有的方法. 我们可以像8.8.10.1节那样显式地通过名字重写接口中的函数.
public ref class EditBox : IControl, IDataBound {
private:
virtual void Paint() = IControl::Paint { … }
vitual void Bind(Binder^ b) = IDataBound::Bind { … }
};
这样实现的接口成员称为显式接口成员(explicit interface members), 因为每个成员都是实现指定的接口的成员.
int main() {
EditBox^ editbox = gcnew EditBox;
editbox->Paint(); // error: Paint is private
IControl^ control = editbox;
control->Paint(); // calls EditBox’s Paint implementation
}
8.11 枚举(Enums)
标准C++中已经支持了枚举类型. C++/CLI在此基础上提供了一些有趣的扩展.
# 一个枚举可以定义为public或private, 这样可以控制它能否被它的父汇编块(assembly)以外的汇编块看到.
# 可以为枚举定义一个underlying type
# 枚举类型和/或者 它的迭代器(enumerator)可以有属性.
# 一种新的语法可以定义强类型的枚举.
8.12 名字空间与汇编块(assembly)
在此之前的程序除了依赖于少数像System::Console这样的类以外基本是独立的. 但是大多数情况下, 现实中的程序一般包含几个块, 每个块分开编译. 比如一个公司的软件可能依赖于几个组件, 一些是自己开发的, 一些是从其它软件商那里买来的.
名字空间和汇编块支持这些基于组件的系统. 名字空间提供了逻辑组织系统. (下面一句不会译: Namespaces are use both as an “internal” organization system for a program, and as an “external” organization system—a way of presenting program elements that are exposed to other programs.)
汇编块是用来物理打包和展开的. 一个汇编块可以包含类. 可执行代码一般会实现这些类然后引用到其它汇编块.
我们再次回到”hello world”程序来看看如何使用名字空间和汇编块, 我们把程序分成两个部分: 一部分是含有显视”hello world”的函数的类库, 一部分是调用那个函数的控制台应用程序.
//DisplayHelloLibrary.cpp
namespace MyLibrary {
public ref struct DisplayMessage {
static void Display() {
Console::WriteLine(“hello, world”);
}
};
}
然后是写一个使用DisplayMessage的控制台应用程序.
//HelloApp.cpp
#using <DisplayHelloLibrary.dll>
int main() {
MyLibrary::DisplayMessage::Display();
}
使用CLI库函数和类时需要include头文件. 取而代之的是, 用#using指令来引用库汇编块. 汇编块名用<…>包着. 代码可以被编译成一个含有DisplayMessage的类库和一个含有main函数的程序. 编译的步骤视编译器而定. 在命令行中可以像下面那样使用.
cl /LD DisplayHelloLibrary.cpp
cl HelloApp.cpp
这样就生成了一个名为DisplayHelloLibrary.dll的类库和一个名为HelloApp.exe的应用程序.
8.13 Versioning
versioning是一个组件随实现推移进行更新换代的过程. 如果依赖以前版本的旧代码可以在新版本中编译, 工作那么就说新版本与以前版本source-compatible. 当旧代码无需重新编译就可以在新版本中工作时, 我们新的版本与以前版本binary-compatible.
考虑这种情况, 一个类的作者写了一个叫Base的类. 在第一个版本, Base不含有函数F. 一个叫Derived的组件继承了Base, 引入了F函数. 这个Derived类和它所依赖的Base类都发布给用户来使用.
public ref struct Base { // version 1
…
};
public ref struct Derived : Base {
virtual void F() {
Console::WriteLine(“Derived::F”);
}
};
目前为止一切都很好. 但是如果Base的作者写一个新的版本, 这个版本有自己的F函数, versioning的问题就出现了.
public ref struct Base { // version 2
virtual void F() {
Console::WriteLine(“Base::F”);
}
};
新版本的Base应该与原版本同时source-compatible和binary-compatible. (如果不可以简单地在基类那里添加一个函数的话, 那么base类就永远不能改进了). 不幸的是新Base的F函数使Derived的F函数不明确了. Derived是重写了Base的F吗?这好像不可能, 因为Derived已经编译过了, 那时的Base还没有F呢! 更进一步说, 如果Drived的F没有重写Base的F, 那么它必须遵守Base指定的合约----这个合约在写Derived前是不存在的. 在某种情况下, 这是不可能的. 比如, Base的F可能要求它的重写总要调用自己(Base’s F might require that overrides of it always call the base). Derived的F是不可能可以尊守这个合约的.
C++/CLI通过允许开发者清楚地声明他们的意图来解决versioning的问题. 在上面刚开始的版本的代码例子中, 因为Base连F都没有, 所以代码是很清晰的. Derived的F是要成为一个新的函数而不是要重写基类的函数.
如果Base更新到了有F的版本, 那么一个编译过的(binary version)Derived的意图依然是清楚的----Derived的F在语义上是独立的, 不应该被看成是重写.
但是当Derived重新编译了, 那么含义就不清楚了----Derived的作者可能希望它的F重写Base的
F也可能希望覆盖Base的F. 在默认情况下, 编译器会让Derived的F重写Base的F. 但是这样做的会导致重新编译后的Derived的行为与之前不同.
如果Derived的F希望与Base的F没有关系, 那么Derived的作者可以用new修饰符来表明它的意图.
public ref struct Base { // version 2
virtual void F() { // add in version 2
Console::WriteLine(“Base::F”);
}
};
public ref struct Derived : Base { // version 2a: new
virtual void F() new {
Console::WriteLine(“Derived::F”);
}
};
另一方面, Derived的作者可能看得更长远一些(investigate further), 决定Derived的F应该重写Base的F. 这种意图可以用override函数显式表明.
public ref struct Base { // version 2
virtual void F() {
Console::WriteLine(“Base::F”);
}
};
public ref struct Derived : Base { // version 2 b: override
virtual void F() override {
Base::F();
Console::WriteLine(“Derived::F”);
}
};
Derived的作者还有一个选择, 就是改F的名字, 这样就可以完全避免了名字碰撞. 虽然这样会打破Derived的source和binary的兼容, 但是兼容的重要性很多情况下依赖于特定的场景. 如果Derived不暴露在其它程序中, 那么改名是一个很好的方法, 因为这样会提高代码可读性.
8.14 Attributes
标准C++中有也有声明的元素. 比如, 类中的函数可访问性可以用public, protect 或者 private来声明. C++/CLI使这种功能一般化了, 这样程序员就可以发明新的声明信息, 绑定这些声明信息到不同的程序实体中, 然后在运行时取得这些声明信息. 程序中用attribute来定义这些额外的声明信息.
比如说, 一个框架可能会定义一个叫HelpAttribute的attribute放在程序的类, 函数等元素中, 使开发者可以把程序中的元素影射到文档中.
[AttributeUsage(AttributeTargets::All)]
public ref class HelpAttribute : Attribute {
String^ url;
public:
HelpAttribute(String^ url) {
this->url = url;
}
String^ Topic;
property String^ Url {
String^ get() { return url; }
}
};
这里定义了一个叫HelpAttribute的attribute类, 它有一个位置参数(String^ url)和一个名字参数(String^ Topic).位置参数是attribute类构造函数的正规参数定义的, 而名字参数则是被公有可读写域和属性定义的. 为了方便, attribute的名字如果后面有”Attribute”的话可以把这个后缀去掉.
[Help(“http://www.mycompany.com/.../Class1.htm’)]
public ref class Class1 {
public:
[Help(“http://www.mycompany.com/.../Class1.htm” , Topic = “F”)]
void F() { }
};
在运行时,我们可以取得程序元素的信息.
int main() {
Type^ type = Class1::typeid;
array<Object^>^ arr =
type->GetCustomAttributes(HelpAttribute::typeid,true);
if ( arr->Length == 0)
Console::WriteLine(“Class1 has no Help attribute.”);
else
HelpAttribute^ ha = (HelpAttribute^) arr[0];
Console::WriteLine(“url = {0}, Topic = {1}”, arr->Url, ha->Topic);
}
}
上面程序检查Class1中是否有Help attribute, 有则输出.
8.15 泛型(generic)
泛型类和函数是一些特点的集合, 它是被CLI定义来提供类型参数的. 泛型与template的不同在于泛型是在运行时被VES(Virtual Execution System)实例化, 而template则是在编译时被编译器实例化. 泛型定义的必须是ref类, value类, interface类, delegate或者函数.
8.15.1 创建和使用泛型
下面, 我们来创建一个Stack 泛型 类, 我们定义类型参数(type parameter)为ItemType, 定义过程中的语法和template是一样的, 只是generic与template两个关键字不同而已. 这个类型参数就像一个占位符那样, 等到定义了类型后才起作用.
generic<typename ItemType>
public ref class Stack {
array<ItemType>^ items;
public:
Stack(int size) {
items = gcnew array<ItemType>(size);
}
void Push(ItemType data) { … }
ItemType Pop() { … }
};
当我们使用Stack时,我们指定泛型类使用的真正类型.
Stack<int>^ s = gcnew Stack<int>(5);
通过这样做, 我们创造了一个型的构造类型(constructed type)----Stack<int>, Stack里面的所有ItemType都换成了int.
如果我们想在Stack中存储其它类型, 我们可以从Stack中创建一个不同的构造类. 比如当我们有一个简单的Customer类而且想用Stack来储存这个类的话, 我们可以简单地把Customer类用作Stack的类型参数, 这样, 我们就轻松地重用了我们的代码.
Stack<Customer^>^ s = gcnew Stack<Customer^>(10);
s->Push(gcnew Customer);
Customer^ c = s->Pop();
当然, 当我们创建了一个容纳Customer的Stack后, 这个Stack就只可以储存Customer了(或者继承自Customer的类), 和template一样, 泛型也提供了强类型.
泛型可以有不限个数的类型参数. 考虑这种情况, 我们需要创建一个简单的存储值和关键字的字典泛型类. 我们像下面这样声明.
generic<typename KeyType, typename ElementType>
public ref class Dictionary {
public:
void Add(KeyType key, ElementType val) { … }
property ElementType default[KeyType] { //indexed property
ElementType get(KeyType key) { … }
void set(KeyType key, ElementType value) { … }
}
};
我们使用Dictionary时, 我们需要在尖括号中提供两个类型参数. 然后当我们调用Add函数或者使用索引属性时, 编译器就会检查我们是否提供了正确类型的参数.
Dictionary<String^, Customer^>^ dict
= gcnew Dictionary<String^, Customer^>;
dict->Add(“Peter”, gcnew Customer);
Customer^ c = dict[“Peter”];
8.15.2 限制
在很多情况下, 我们希望做得更多, 而不是仅仅根据所提供的类型储存数据. 我们也经常需要用到类型参数所代表的类型里面的成员数据或者函数. 比如说在Dictionary的Add函数中我们需要用关键字类型的CompareTo函数来比较两个部件(item)是否相等.
generic<typename KeyType, typename ElementType>
public ref class Dictionary {
public:
void Add(KeyType key, ElementType val) {
…
if(key->CompareTo(val) < 0) { … } //compile-time error
…
}
};
很不幸, 在编译时类型参数的KeyType是泛型. 像上面这样写, 编译器只会认为那些在System::Object中像ToString那样的函数才能够在KeyType的key中使用. 所以编译器会因为找不到CompareTo函数而报错. 然而我们可以把变量key转换成一个含有CompareTo的类, 比如说IComparable接口, 这样程序便可通过编译.
generic<typename KeyType, typename ElementType>
public ref class Dictionary {
public:
void Add(KeyType key, ElementType val) {
…
if( static_cast<IComparable^>(key)->CompareTo(val) < 0) { … }
…
}
};
但是如果现在我们用一个没有实现IComparable的类作为Dictionary的关键字类, 运行时就会报错(这里是System::InvalidCastException). 因为泛型的一个目的是提供强类型和减少类型转换, 所以我们需要一个更好的解决方案.
我们可以替供一个不是必须的清单来限制(constraint)类型参数. 限制就是类型参数只可以接受符合某种条件的类型. (比如: 可以限制那个类必须实现了某个接口, 或继承了某个类) 限制是以关键字where开头然后是要限制的类型参数, 在参数后面加上( : ), 然后是用逗号隔开的类或接口.
为了在Dictionary里面使用CompareTo, 我们可以限制KeyType, 限制它必须实现了IComparable接口.
generic<typename KeyType, typename ElementType>
where KeyType : IComparable
public ref class Dictionary {
public:
void Add(KeyType key, ElementType val) {
…
if ( key->CompareTo(val) < 0) { … }
…
}
};
在编译时, 每次我们构造一个Dictionary类的时候这个代码就会检查构造是否合法. 这样我们就不用显式地把传入的类型转换成IComparable了.
在一个有限制的框架环境中使用限制是非常有用的.(i.e., a collection of related classes, where it is advantageous to ensure that a number of types support some common signatures and/or base types.) 限制可以用来定义泛型算法(“generic algorithm”)把提供给不同的类的函数连接起来. 这也可以用子集和运行时多态来实现, 但是很多情况下用限制来实现可以得到更高效的代码, 更有弹性的泛型算法定义, 更容易在编译时发现错误. 但是, 使用限制需要小心以及尝试(taste). 没有实现限制中的要求的类是不可以连接在泛型代码中的.
对于类型参数, 我们可以在它的限制中指明任意个数的接口, 但是不可以多于一个基类. 类型参数的限制用关键字where隔开.
generic<typename KeyType, typename ElementType>
where KeyType : IComarable, IEnumerable
where ElementType : Customer
public ref class Dictionary {
public:
void Add(KeyType key, ElementType val) {
…
if ( key->CompareTo(val) < 0 ) { … }
…
}
};
8.15.3 泛型 函数
有时候, 不是整个类需要类型参数, 而是一个函数需要类型参数. 这通常发生在创建一个带泛型参数的函数. 比如在我们使用前面介绍的Stack, 我们可能经常向Stack里压入几个值, 决定写一个函数来完成这个任务.
我们用泛型函数来解决这个问题. 和泛型类一样, 泛型函数也是以关键字generic和一个类型参数列表开头. 和template函数一样, 类型列表中的类型可以用在参数类型, 返回类型, 和函数体中. 一个名为PushMultiple的泛型函数看上去像这样.
generic<typename StackType, typename ItemType>
where ItemType : StackType
void PushMultiple(Stack<StackType>^ s, …array<ItemType>^ values) {
for each (ItemType v in values) {
s->Push(v);
}
}
用了这个泛型函数我们就可以把不同的部件放到不同的Stack里. 更重要的是, 由于有限制的存在, 编译器会保证压入Stack的对象的类型是符合要求的. 调用泛型函数时, 我们把类型参数放到尖括号里, 像下面那样.
Stack<int>^ s = gcnew Stack<int>(5);
PushMultiple<int, int>(s, 1, 2 ,3 ,4);
调用这个函数时显式为StackType和ItemType提供了类型. 但是在很多情况下编译器可以使用一种叫type deduction的方法从提供给函数的那些参数中推导出正确的类型. 在上面的例子中, 传递的第一个参数是Stack<int>, 后面的则是int, 这样编译器可以知道类型参数是int. 因此, 泛型的PushMultiple函数可以不用指明类型参数而直接调用.
Stack<int>^ s = gcnew Stack<int>(5);
PushMultiple(s, 1, 2, 3, 4);
本章结束