Standard ECMA-372 1st Edition / December 2005
C++/CLI Language Specification
C++/CLI 语言详述
译者:Enzo Yang
8.7 Delegates
Delegate为经常使用标准C++库的函数适配器(function adapter)的程序员提供了一种方案.
一个delegate的定义是隐式地定义一个继承自System::Delegate的类. 一个delegate实例把一个或多个函数封装在一个调用清单里(invocation list), 清单里的每个成员都代表着一个可调用实体(callable entity). 对于实例函数来说, 一个可调用实体是一个实例和实例里面的成员函数. 对于静态函数或者全局函数或者名字空间域函数来说, 一个可调用得实体就是那个函数. 给出一个delegate实例和一些合适的参数, 你可以调用所有delegate实例里面的可调用实体, 传递给那个实体同样的参数.
比如说下面的例子:
delegate void MyFunction( int value); //define a delegate type
public ref struct A {
static void F(int i) { Console::WriteLine(“F:{0}”, i); }
};
public ref struct B {
void G( int i) { Console::WriteLine(“G:{0}”, i); }
}
静态函数A::F和实例函数B::G参数类型和返回类型都与MyFunction一样, 所以它们可以被封装在MyFunction中. 需要注意的是它们的是否与MyFunction兼容与它们的可访问性无关. 只要程序员觉得合适的话, 这样的函数(要被封装的函数)同样可以被定义在同一个或不同的类里面.
int main() {
MyFunction^ d; //create a delegate reference
d = gcnew MyFuction(&A::F); //invocation list is A::F
d(10);
B^ b = gcnew B;
d += gcnew MyFunction(b, &B::G); // invocation list is A::F B::G
d(20);
d+= gcnew MyFunction(&A::F); //invocation list is A::F B::G A::F
d(30);
d -= gcnew MyFunction(b, &B::G); //invocation list is A::F A::F
d(40);
F:10
F:20
G:20
F:30
G:30
F:30
F:40
F:40
当函数不是一个静态的成员函数时, 构造一个delegate需要两个参数: 第一个是那个ref class的实例, 第二个是那个ref class中的非静态成员函数的成员指针. 当函数是一个静态成员函数或者非成员函数时, 构造一个delegate只要一个参数, 该参数是到那个函数的指针.
两个兼容的delegate调用清单可以用 += 操作符合并, 另外, 可调用实体可以用 -= 操作符从调用清单中清除. 但是一但调用清单被创建了它就不可以更改了. 值得注意的是, 上面两个操作符实际上是创建了一个新的调用清单.
一旦一个delegate实例被初始化了, 我们就可以间接地像直接调用那样调用封装在里面的函数(调用的顺序和它们被添加到delegate里面的顺序一样). 调用delegate返回的值是清单中最后一个返回的值. 如果试图调用一个空的调用清单, 系统会返回一个NullReferenceException异常.
8.8 本机类和引用类(native and ref classes)
8.8.1 literal 域
一个literal 域(field)是一个代表编译时常右值(constant rvalue)的域. literal域的值允许建立在同一个程序中在它之前定义的另一个literal域的基础上. 比如:
ref class X {
literal int A = 1;
public:
literal int B = A + 1;
};
ref class Y {
public:
literal double c = X::B * 5.6;
};
上面展示了两个类, 在他们里面定义了3个literal域, 其中两个是public另一个是private.
虽然得到literal域的语法和得到static成员的语法是相似的, 但是literal域不是static的而且它的定义不需要也不允许加上static关键字. literal域可以通过类名得到.
int main() {
cout << “B = “ << X::B << “/n”;
cout << “C = “ << Y::C << “/n”;
}
输出结果是
B = 2
C = 11.2
literal域只可以在ref, value, 和 interface 类里面使用.
8.8.2 Initonly 域
initonly声明符声明了一个域, 当且仅当它在成员初始化列表(ctor-initializer)或构造函数里时它是一个左值. 其它情况下它是右值. 这样的域叫做initonly域. 比如:
public ref class Data {
initonly static double coefficient1;
initonly static double coefficient2;
static Data() {
coefficient1 = … ; // ok
coefficient2 = … ; // ok
}
public:
static void F() {
coefficient1 = … ; // error
coefficient2 = … ; // error
}
};
对initonly域的赋值只可以是域定义的一部分, 或者在实例的构造函数中, 或者在static构造函数中. (一个static的initonly域只可以被static构造函数赋值, 非static的initonly域只可以被实例的构造函数赋值).
initonly域只可以在ref和value类中使用.
8.8.3 函数
CLI类中的成员函数的定义和使用方法和标准C++是差不多的. 但是, C++/CLI还是有一些不同点. 比如:
# const和volatile修饰符不可以添加在非静态成员函数上.
# 函数修饰符override和重写定义提供了一个显式重写和重写特定名字的函数的功能(参考8.8.10.1节)
# 可以用关键字sealed把虚拟函数封起来,不让继承类重写掉.
# abstract修饰符提供了另外一种声明抽象函数的方法
# 函数修饰符new允许函数隐藏基类与它同名同参数列表,同cv-quanlification(const和volatile)的函数. 这样的隐藏不是重写掉基类的函数, 甚至被隐藏的函数式virtual也不会被重写.
# 类型安全的不定个数定参数可以使用参数列表实现.
8.8.4 属性
一个属性是一个表现得自己像域一样的地成员. 有两种不同的属性: 标量(scalar)和索引(indexed). 标量属性(scalar property)提供了一种域的方法来处理类或CLI对象. 标量属性的例子包括string的length, font的size, window的caption, customer的name. 索引属性提供了一种数组的方法来处理CLI对象. 索引属性的一个例子是bit-array类.
属性是域的一个革命性扩展----它们都是某一个类的成员, 同样的调用语法(注: 原文是这样写的the syntax for accessing scalar fields and scalar properties is the same, as is that for accessing CLI arrays and indexed properties). 但是, 与域不同的是, 属性不是指示(denote)存储位置而是定义了存取函数(accessor function).
属性的定义有自己的语法. 定义的第一部分和域定义比较相似. 第二部分包括一个”取”函数和/或者 一个”存”函数. 如果属性拥有”取”函数和”存”函数那么我们就既可以读也可以写那个属性. 下面例子Point类定义了两个读写属性, X和Y.
public value class Point {
int Xor;
int Yor;
public:
property int X {
int get() { return Xor; }
void set( int value) { Xor = value;}
}
property int Y {
int get() { return Yor; }
void set(int value) { Yor = value; }
}
void Move( int x, int y) { // absolute move
X = x;
Y = y;
}
void Translate( int x, int y) { // relative move
X += x;
Y += y;
}
…
};
当属性被读的时候get函数被调用, 当属性被写的时候set函数被调用.
属性的定义相对来说是比较直观的, 但是它所代表真正的值只有用到时我们才能看到.比如说X和Y属性可以像域一样供我们读写. 在上面的例子中, 属性被用来隐藏类里面的数据. 下面代码同样直接或间接地用到了属性.
Point p1; // set to (0, 0)
p1.X = 10; // set to (10,0)
p1.Y = 5; // set to (10, 5)
p1.Move(5, 7); // move to (5, 7)
Point p2(9, 1); // set to (9, 1)
p2.Translate(-4, 12); // move 4 left and 12 up, to (5, 13)
对于一些简单的属性(trivial property)的声明
property String^ Name;
编译器自动提供默认的存取函数.
一个默认索引属性(default-indexed property)使我们可以像读取数组一样读取一个实例. [note: 其他语言可能会把default-indexed写成indexer. end note]
下面以一个Stack类为例. 这个类的设计者可能想提供一种数组式的操作来查看或更改Stack里面元素. 这个Stack用链表实现,但是提供了方便的数组式访问.
默认索引属性的定义和普通的属性定义相似, 最大的不同就是默认索引属性是没有名字的而且它有索引参数.索引参数是放在方括号中的.
public ref class Stack {
public:
ref struct Node {
Node^ Next;
Object^ Value;
Node(Object^ value) : Next(nullptr), Value(value) { }
Node(Object^ value, Node^ next) {
Next = next;
Value = value;
}
};
private:
Node^ first;
Node^ GetNode(int index) {
Node^ temp = first;
While (index > 0) {
temp = temp->Next;
index--;
}
return temp;
}
bool ValidIndex(int index) { … }
public:
property Object^ default[int] { // default-indexed property
Object^ get(int index) {
if (!ValidIndex(index))
throw gcnew Exception(“Index out of range.”);
else
return GetNode(index)->Value;
}
void set(int index, Object^ value) {
if( !ValidIndex(index))
throw gcnew Exception(“Index out of range.”);
else
GetNode(index)->Value = value;
}
}
Object^ Pop() { … }
void Push(Object^ o) { … }
…
};
int main() {
Stack^ s = gcnew Stack;
s->Push(1);
s->Push(2);
s->Push(3);
s[0] = 33; // The top item now refers to 33 instead of 3
s[1] = 22; // The middle item now refers to 22 instead of 2
s[2] = 11; // The bottom item now refers to 11 instead of 1
}
[note: 更有效率实现Stack的方法是使用泛型. end note]
8.8.5 事件(Events)
事件用于为CLI对象提供通知的成员. 在类中声明事件的语法与声明域相似, 而且声明事件时可以决定是否为它指定一些存取函数. 事件的类型必须是一个delegate类的句柄(参看8.7)
public delegate void EventHandler(Object^ sender, EventArgs^ e);
public ref class Button {
public:
event EventHandler^ Click;
};
Button类定义了一个EventHandler类型的Click事件. Click成员只可以在符号 += 和 -=的左边使用, 或者像函数那样被调用(这种情况下Click里面的所有函数都会被调用). 操作符+=给事件添加处理函数, 操作符-=去掉事件中特定的一个处理函数.
public ref class Form1 {
Button^ Button1;
void Button1_Click(Object^ sender, EventArgs ^e) {
Console::WriteLine(“Button1 was clicked!”);
}
public:
Form() {
Button1 = gcnew Button;
// Add Button1_Click as an event handler for Button1’s Click event
Button->Click += gcnew EventHandler(this, &Form1::Button1_Click);
}
void Disconnect() {
Button1->Click -= gcnew EventHandler(this, &Form1::Button1_Click);
}
};
上面展示了Form1类, 它把Button1_Click添加为Button1的Click事件的一个处理函数. 在Disconnect函数中它把Button1_Click从事件中移除.
如果程序员要取得更多的控制权的话, 可以自己提供存取函数. 比如, Button 函数可以像下面这样写:
public ref class Button {
EventHandler^ handler;
public:
event EventHandler^ Click {
void add(EventHandler^ e) { handler += e ; }
void remove(EventHandler^ e) { handler -= e; }
}
…
};
这个变化对用户的代码没有影响. 但是使得Button类的实现更具弹性. 比如说, 一个Click的事件处理函数不一定要代表一个域(原文: For example, the event handler for Click need not be represented by a field)
对于像下面那样简单的事件(trivial event)
event EventHandler^ Click;
编译器会自动提供存取函数的实现.
8.8.6 静态操作符
除了可以重载标准C++的操作符之外, C++/CLI还可以定义静态的 或/和 含有^类的参数.
下面的例子展示了整数容器类的一部分:
public ref class IntVector {
array<int>^ values;
public:
property int length { //property
int get() {return values->Length; }
}
property int default[int] { // default-indexed property
int get(int index) { return values[index]; }
void set(int index, int value) { values[index] = value; }
}
IntVector(int length);
IntVectro(int length, int value);
// unary – (nagation)
static IntVector^ operator-(IntVector^ iv) {
IntVector temp = gcnew IntVector(iv->Length);
for (int i = 0 ; I < iv->Length; ++i) {
temp[i] = -iv[i];
}
return temp;
}
static IntVector^ operator+(IntVector^ iv, int val) {
IntVector^ temp = gcnew IntVector(iv->Length);
for ( int i = 0 ; i < iv->Length; ++i) {
temp[i] = iv[i] + val ;
}
return temp;
}
static IntVector^ operator+(int val, IntVector^ iv) {
return iv + val;
}
…
};
int main() {
IntVector^ iv1 = gcnew IntVector(4); // 4 elements with value 0
IntVector^ iv2 = gcnew IntVector(7, 2); // 7 elements with value 2
iv1 = -2 + iv2 + 5;
iv2 = -iv1;
}
8.8.7 构造函数
与标准C++不同的是, C++/CLI支持静态构造函数(参看8.8.9节). 这样标准C++的构造函数的准确定义应该为实例构造函数(instance constructor).
8.8.8 析构函数和finalizer(不知道中文术语是什么, 希望知道的朋友可以告诉我)
在标准C++中, 用于清理的代码一般都封装在析构函数里面. 这种方法在提供了强大而方便的途径来处理抽象资源的同时存在着内存泄漏的问题(如果没有调用到析构函数的话). 有了垃圾收集器, C++/CLI提供了一个写用于清理的代码的机制, 如果没有任何对象能够引用一个对象时它可以调用那个对象的析构函数. 这样的话一个ref类就有了两种特殊的成员函数来清理资源:一个析构函数(destructor)一个finalizer.
# Destructor:析构函数提供了确定的清除(deterministic cleanup)然后结束那个对象的生命周期.和标准C++那样, 析构函数以与构造函数相反的顺序清理掉基类和成员数据. 在每个ref类里, 析构函数依次执行用户写的析构代码, 调用成员数据的析构函数, 调用基类的析构函数. 析构函数的最大好处是它在程序的确定的一点执行, 这样会比等待垃圾回收早一点把不需要的资源清除掉.
# Finalizer: finalizer提供不确定的清除(non-deterministic cleanup), 一个finalizer是对象在垃圾收集器中运行的最后机会(last-chance), 一般是对象没有调用到析构函数的清况下执行. Finalizer可以有效保证资源没有调用析构函数情况下被释放(特别是对于那些本机堆中的资源). Finalizer是在垃圾收集器确定再没有东西可以应用那个对象后的一段时间内调用的. (拥有finalizer会损失一点性能)
一个拥有资源的ref类总应该有一个析构函数. 一个有Finalizer的ref类也总应该有一个析构函数, 这样好让资源早日释放. 但是一个有析构函数的类不必需有一个Finalizer.
ref struct R {
~R() { … } // destructor, but no finalizer
};
一个ref类的实例如果有用值类型指向的资源(比如说指针)的话, 应该有一个Finalizer. (这可能对那些基类没有finalizer的类造成性能损失. 因此, 一个设计得好的类分层结构会限制值类型指向的资源离开这个分层结构.) 一个没有指向资源的值类型的ref类实例可以有析构函数, 但不应该有Finalizer.
ref struct R {
~R() { … } // destructor
!R() { … } ` // finalizer
};
C++/CLI用CLI处理样式(CLI dispose pattern)在所有的ref类中实现析构函数和Finalizer, 这些样式包括5个函数(Dispose(), Dispose(bool), Finalize(), __identifier(“~T”)(), 和 __identifier(“!T”)()), 所有这些定义都是编译器在需要时产生的. 这些清理机制不是对C++/CLI程序员可见的. 在C++/CLI中, 正确进行清理工作的方法是把所有用于清理的代码放到析构函数和Finalizer里面.
# Finalizer应该清理所有值类型指向的资源.
# 析构函数应该尽可能做最多的清除工作. 为了方便, 程序员应该在析构函数中调用Finalizer和写其它一些用于清理的代码. 析构函数可以安全地得到对象中引用的ref类的状态, 而Finalizer则不能.
对于ref类, Finalizer和析构函数都必须写, 这样他们就可以被执行几次或者可以在没有完全构造的对象上执行.
8.8.9 静态构造函数
静态构造函数(static constructor)是ref类或value类的静态成员, 它用来初始化类中的静态成员, 不能用于初始化非静态成员. 静态构造函数不可以有参数, 必须是私有的, 不可以直接调用. 静态构造函数是在运行时自动调用的. [Note: 静态构造函数需要声明为私有的来阻止多次调用. end note]
public ref class Data {
private:
initonly static double coefficient1;
initonly static double coefficient2;
static Data() {
// read in the value of the coefficients form some source
coefficient1 = …;
coefficient2 = …;
}
public:
…
};
上面显视了Data类中的静态构造函数. 它初始化了两个initonly的静态域.
8.8.10 继承
在用ref类时,C++/CLI只支持单继承ref类. 但是可以继承多个接口.
8.8.10.1 函数重写
在标准C++中,子类和基类如果有一个同样名字,参数列表,和cv-quanlification的函数(其中基类那个函数为virtual), 那么子类总会重写掉基类的那个函数, 即使子类的函数没有声明为virtual.
struct B {
virtual void f();
virtual void g();
};
struct D : B {
virtual void f(); // D::f overrides B::f
void g(); // D::g overrides B::g
};
我们称这些为隐式重写(implicit overriding). (D::f的virtual定义符是可选的, 这个virtual实际上不是显式重写的标志.) 由于隐式重写阻碍了versioning(参看8.13节), 隐式重写必须被C++/CLI编译器检查出来.
C++/CLI的虚拟函数重写有两个特点是标准C++不具备的, 这些特点在ref类中可用. 它们分别是显式重写和指定名字重写(named overriding).
显式重写: 在C++/CLI中可以像下面那样声明.
1. 子类函数用修饰符override显式重写一个基类同名同参数同cv-qualification的虚拟函数. 如果基类没有这样的虚拟函数那么这个程序时错误的.
2. 如果子类不想重写掉基类的同名同参数同cv-qualification的虚拟函数可以用new修饰符显式声明.
ref struct B {
virtual void F() {}
virtual void G() {}
};
ref struct D : B {
virtual void F() override {} // D::F override B::F
virtual void G() new {} // D::G doesn’t override B::G, it hides it
};
D::F必须是virtual, 而且必须那样写上virtual. D::G则不一定要是virtual, 如果它不是virtual的它就不应该写上virtual.
名字重写: 除了使用override修饰符我们可以用重写定义符(override-specifier)来做同样的事. 它包括指定我们要重写的函数的名字. 这个方法也可以让我们重写不同名字但参数列表相同的函数.
ref struct B {
virtual void F() {}
};
interface struct I {
virtual void G();
};
ref struct D : B, I {
virtual void X() = B::F, I::G {} // D::X overrides B::F and I::G
};
使用重写定义符的所有函数必须是virtual的.
显式重写和名字重写是可以结合的, 就像下面那样.
ref struct B {
virtual void F() {}
virtual void G() {}
};
ref struct D : B {
Virtual void F() override = B::G {}
};
一个函数只可以被重写一次. 所以, 如果隐式重写和名字重写重写了同一个参数的话, 程序就会出错.
ref struct B {
virtual void F() {}
virtual void G() {}
};
ref struct D : B {
virtual void F() override = B::F {} // Error: B::F is overriden twice
virtual void G() override {} // B::G is overriden
virtual void H() = B::G {} // Error: B::G is overriden twice
};
[Note: 如果基类是一个模板的类参数, 那么名字重写不会在实例化前实现.
template<typename T>
ref struct R : T {
virtual void F() = T::G {}
};
T::G是一个不定的名字. end note]