2.4.4声明一个引用对象
修订版语言支持在本地栈上声明引用类的对象,或者声明为类的成员,就像它可以直接被访问一样(注意这在 Microsoft Visual Studio 2005 的Beta1 发布版中不可用)。析构函数和在 2.4.3 节中描述的 Dispose()方法结合时,结果就是引用类型的终止语义的自动调用。使 CLI 社区苦恼的非确定性终止这条暴龙终于被驯服了,至少对于 C++/CLI的用户来说是这样。让我们看一下这到底意味着什么。
首先,我们这样定义一个引用类,使得对象创建函数在类构造函数中获取一个 资源。其次,在类的析构函数中,释放对象创建时获得的资源。
public ref class R { public: R() { /* 获得外部资源 */ } ~R(){ /* 释放外部资源 */ } // ... 杂七杂八 ... };
对象声明为局部的,使用没有附加"帽子"的类型名。所有对对象的使用(如调用成员函数)是通过成员选择点 (.) 而不是箭头 (->) 完成的。在块的末尾,转换成 Dispose()的相关的析构函数被自动调用。
void f() { R r; r.methodCall(); // ... // r被自动析构 - // 也就是说, r.Dispose() 被调用... }
相对于 C#中的 using语句来说,这只是语法上的点缀而已,而不是对基本 CLI约定(所有引用类型必须在 CLI堆上分配)的违背。基础语法仍未变化。用户可能已经编写了下面同样功能的语句(这很像编译器执行的内部转换):
// 等价的实现... // 除了它应该位于一个 try/finally 语句中之外 void f() { R^ r = gcnew R; r->methodCall(); // ... delete r; }
事实上,在修订版语言设计中,析构函数再次与构造函数配对成为和一个局部对象生命周期关联的自动获得/释放资源的机制。这个显著的成就非常令人震惊,并且语言设计者应该因此被大力赞扬。
2.4.5声明一个显式的Finalize()-(!R)
在修订版语言设计中,如我们所见,构造函数被合成为 Dispose()方法。这意味着在析构函数没有被显式调用的情况下,垃圾回收器在终止过程中,不会像以前那样为对象查找相关的 Finalize()方法。为了同时支持析构函数和终止,修订版语言引入了一个特殊的语法来提供一个终止器。举例来说:
public ref class R { public: !R() { Console::WriteLine( "I am the R::finalizer()!" ); } };
!前缀表示引入类析构函数的类似符号 (~),也就是说,两种后生命周期的方法名都是在类名前加一个符号前缀。如果派生类中有一个合成的 Finalize()方法,那么在其末尾会插入一个基类的 Finalize()方法的调用。如果析构函数被显式地调用,那么终止器会被抑制。这个转换如下所示:
// V2 中的内部转换 public ref class R { public: void Finalize() { Console::WriteLine( "I am the R::finalizer()!" ); } };
2.4.6这在V1到V2的转换中意味着什么
这意味着,只要一个引用类包含一个特别的析构函数,一个 V1程序在 V2 编译器下的运行时行为被静默地修改了。需要的转换算法如下所示:
• |
如果析构函数存在,重写它为类终止器方法。 |
• |
如果 Dispose()方法存在,重写到类析构函数中。 |
• |
如果析构函数存在,但是 Dispose()方法不存在,保留析构函数并且执行第 (1) 项。 |
在将代码从 V1移植到 V2的过程中,可能漏掉执行这个转换。如果应用程序某种程度上依赖于相关终止方法的执行,那么应用程序的行为将被静默地修改。
属性和操作符的声明在修订版语言设计中已经被大范围重写了,隐藏了原版设计中暴露的底层实现细节。另外,事件声明也被修改了。
在 V1中不受支持的一项更改是,静态构造函数现在可以在类外部定义了(在 V1中它们必须被定义为内联的),并且引入了委托构造函数的概念。
在原版语言设计中,每一个 set或者 get属性存取方法都被规定为一个独立的成员函数。每个方法的声明都由 __property关键字作为前缀。方法名以 set_或者 get_开头,后面接属性的实际名称(如用户所见)。这样,一个获得向量的 x坐标的属性存取方法将命名为 get_x,用户将以名称 x来调用它。这个名称约定和单独的方法规定实际上反映了属性的基本运行时实现。例如,以下是我们的向量,有一些坐标属性:
public __gc __sealed class Vector { public: // ... __property double get_x(){ return _x; } __property double get_y(){ return _y; } __property double get_z(){ return _z; } __property void set_x( double newx ){ _x = newx; } __property void set_y( double newy ){ _y = newy; } __property void set_z( double newz ){ _z = newz; } };
这使人感到迷惑,因为属性相关的函数被展开了,并且需要用户从语法上统一相关的 set和 get。而且它在语法上过于冗长,并且感觉上不甚优雅。在修订版语言设计中,这个声明更类似于 C# — property 关键字后接属性的类型以及属性的原名。set存取和get存取方法放在属性名之后的一段中。注意,与 C# 不同,存取方法的符号被指出。例如,以下是上面的代码转换为新语言设计后的结果:
public ref class Vector sealed { public: property double x { double get() { return _x; } void set( double newx ) { _x = newx; } } // Note: no semi-colon ... };
如果属性的存取方法表现为不同的访问级别 — 例如一个公有的 get和一个私有的或者保护的 set,那么可以指定一个显式的访问标志。默认情况下,属性的访问级别反映了它的封闭访问级别。例如,在上面的 Vector定义中,get和 set方法都是公有的。为了让 set方法成为保护或者私有的,必须如下修改定义:
public ref class Vector sealed { public: property double x { double get() { return _x; } private: void set( double newx ) { _x = newx; } } // 注意:private 的作用域到此结束 ... //注意:dot 是一个 Vector 的公有方法... double dot( const Vector^ wv ); // etc. };
属性中访问关键字的作用域延伸到属性的结束括号或者另一个访问关键字的说明。它不会延伸到属性的定义之外,直到进行属性定义的封闭访问级别。例如,在上面的声明中,Vector::dot()是一个公有成员函数。
为三个 Vector坐标编写 set/get属性有点乏味,因为实现的本质是定死的:(a) 用适当类型声明一个私有状态成员,(b) 在用户希望取得其值的时候返回,以及 (c) 将其设置为用户希望赋予的任何新值。在修订版语言设计中,一个简洁的属性语法可以用于自动化这个使用方式:
public ref class Vector sealed { public: //等价的简洁属性语法 property double x; property double y; property double z; };
简洁属性语法所产生的一个有趣的现象是,在编译器自动生成后台状态成员时,除非通过 set/get访问函数,否则这个成员在类的内部不可访问。这就是所谓的严格限制的数据隐藏!
原版语言对索引属性的支持的两大缺点是不能提供类级别的下标,也就是说,所有索引属性必须有一个名字,举例来说,这样就没有办法提供可以直接应用到一个 Vector或者Matrix类对象的托管下标操作符。其次,一个次要的缺点是很难在视觉上区分属性和索引属性 — 参数的数目是唯一的判断方法。最后,索引属性具有与非索引属性同样的问题 — 存取函数没有作为一个基本单位,而是分为单独的方法。举例来说:
public __gc class Vector; public __gc class Matrix { float mat[,]; public: __property void set_Item( int r, int c, float value); __property int get_Item( int r, int c ); __property void set_Row( int r, Vector* value ); __property int get_Row( int r ); };
如您所见,只能用额外的参数来指定一个二维或者一维的索引,从而区分索引器。在修订版语法中,索引器由名字后面的方括号 ([,]) 区分,并且表示每个索引的数目和类型:
public ref class Vector; public ref class Matrix { private: array<float, 2>^ mat; public: property int Item [int,int] { int get( int r, int c ); void set( int r, int c, float value ); } property int Row [int] { int get( int r ); void set( int r, Vector^ value ); } };
在修订版语法中,为了指定一个可以直接应用于类对象的类级别索引器,重用 default关键字以替换一个显式的名称。例如:
public ref class Matrix { private: array<float, 2>^ mat; public: //OK,现在有类级别的索引器了 // // Matrix mat ... // mat[ 0, 0 ] = 1; // // 调用默认索引器的 set 存取函数... property int default [int,int] { int get( int r, int c ); void set( int r, int c, float value ); } property int Row [int] { int get( int r ); void set( int r, Vector^ value ); } };
在修订版语法中,当指定了 default索引属性时,下面两个名字被保留:get_Item和set_Item。这是因为它们是 default索引属性产生的底层名称。
注意,简单索引语法与简单属性语法截然不同。
声明一个委托和普通事件仅有的变化是移除了双下划线,如下面的示例所述。在去掉了之后,这个更改被认为是完全没有争议的。换句话说,没有人支持保持双下划线,所有人现在看来都同意双下划线使得原版语言感觉很难看。
// 原版语言 (V1) __delegate void ClickEventHandler(int, double); __delegate void DblClickEventHandler(String*); __gc class EventSource { __event ClickEventHandler* OnClick; __event DblClickEventHandler* OnDblClick; // ... }; // 修订版语言 (V2) delegate void ClickEventHandler( int, double ); delegate void DblClickEventHandler( String^ ); ref class EventSource { event ClickEventHandler^ OnClick; event DblClickEventHandler^ OnDblClick; // ... };
事件(以及委托)是引用类型,这在 V2中更为明显,因为有帽子 (^) 的存在。除了普通形式之外,事件支持一个显式的声明语法,用户显式指定事件关联的 add()、raise()、和 remove()方法。(只有 add()和 remove()方法是必须的;raise()方法是可选的)。
在 V1设计中,如果用户选择提供这些方法,尽管她必须决定尚未存在的事件的名称,她也不必提供一个显式的事件声明。每个单独的方法以 add_EventName、raise_EventName、和 remove_EventName的格式指定,如以下引用自 V1语言规范的示例所述:
// 原版 V1 语言下 // 显式地实现 add、remove 和 raise ... public __delegate void f(int); public __gc struct E { f* _E; public: E() { _E = 0; } __event void add_E1(f* d) { _E += d; } static void Go() { E* pE = new E; pE->E1 += new f(pE, &E::handler); pE->E1(17); pE->E1 -= new f(pE, &E::handler); pE->E1(17); } private: __event void raise_E1(int i) { if (_E) _E(i); } protected: __event void remove_E1(f* d) { _E -= d; } };
该设计的问题主要是感官上的,而不是功能上的。虽然设计支持添加这些方法,但是上面的示例看起来并不是一目了然。因为 V1属性和索引属性的存在,类声明中的方法看起来千疮百孔。更令人沮丧的是缺少一个实际的 E1事件声明。(再强调一遍,底层实现细节暴露了功能的用户级别语法,这显然增加了语法的复杂性。)这只是劳而无功。V2设计大大简化了这个声明,如下面的转换所示。事件在事件声明及其相关委托类型之后的一对花括号中指定两个或者三个方法如下所示:
// 修订版 V2 语言设计 delegate void f( int ); public ref struct E { private: f^ _E; //是的,委托也是引用类型 public: E() { // 注意 0 换成了 nullptr! _E = nullptr; } // V2 中显式事件声明的语法聚合 event f^ E1 { public: void add( f^ d ) { _E += d; } protected: void remove( f^ d ) { _E -= d; } private: void raise( int i ) { if ( _E ) _E( i ); } } static void Go() { E^ pE = gcnew E; pE->E1 += gcnew f( pE, &E::handler ); pE->E1( 17 ); pE->E1 -= gcnew f( pE, &E::handler ); pE->E1( 17 ); } };
虽然在语言设计方面,人们因为语法的简单枯燥而倾向于忽视它,但是如果对语言的用户体验有很大的潜移默化的影响,那么它实际上很有意义。一个令人迷惑的、不优雅的语法可能增加开发过程的 风险,很大程度上就像一个脏的或者不清晰的挡风玻璃增加开车的风险一样。在修订版语言设计中,我们努力使语法像一块高度磨光的新安装的挡风玻璃一样透明。