就某些类而言
当在程序中第一次使用时
最好能有一个初始化过程
当程序不再需要时
也最好能做一些收尾工作
这些都是非常好的类设计习惯
引出问题
如果有这样一种情况某种类型的每个实例都必须有其唯一的ID比如说某种交易类型这些ID可用于在处理过程中追蹤每笔交易或之后用于审计员查看数据文件为讨论方便此处的ID为从起始的有符号整型数
如果把一个nextID值保存在内存中并在每个新实例构造时把它递增这无疑是一个不错的想法但是为使在程序连续的执行过程中保持ID值的唯一就需要在每次程序结束时保存此值并在下次程序开始运行时恢复这个值但在标准C++中是没办法来达到这个目的的实际上使用标准CLI库也同样没办法完成然而在CLI的NET实现中有几个扩展库它们却可以完成这个任务
问题重现
这回又用到了Point类因为带有唯一ID的点很适合此主题例中的程序输出在代码之后
例
using namespace System;
Point F(Point p) {
return p;
}
int main()
{
/**/ Point::TraceID = true;
/**/ Point^ hp = gcnew Point;
Console::WriteLine(hp: {} hp);
/**/ hp>Move();
Console::WriteLine(hp: {} hp);
/**/ Point^ hp = gcnew Point();
Console::WriteLine(hp: {} hp);
/**/ Point p p();
Console::WriteLine(p: {} p: {} %p %p);
/**/ p = F(p);
Console::WriteLine(p: {} %p);
}
输出
hp: []()
hp: []()
hp: []()
p: []() p: []()
p: []()
在程序开始运行时从一个文本文件中读取下一个可用的ID值并用它来初始化一个Point类中的私有静态(private static)字段最开始这个文件包含的值为零
基于公共静态布尔属性TraceID的值Point中ToString函数生成的字符串可有选择地包含Point的ID并以 [id] 的形式作为一个前缀如果此属性值为true就包含ID前缀否则就不包含默认情况下这个属性值被设为false因此在标号中我们把它设为true
在标号中使用默认构造函数为Point分配了内存空间并显示它的ID为及值为()在标号中通过Move函数修改了Point的x与y坐标值但这不会修改Point的ID毕竟它仍是同一个实例只不过用了不同的值接着在标号中使用了接受两个参数的构造函数为另一个Point分配了内存空间并显示它的ID为及值为()
在标号中创建了两个基于堆栈的实例并显示出它们的ID及值在第三个及第四个Point创建时它们的ID分别为和
在标号中p被赋于了一个新值然而p仍是它之前的同一个Point所以它的ID没有改变
第二次运行程序时输出如下
hp: []()
hp: []()
hp: []()
p: []() p: []()
p: []()
如上所示个新实例都被赋于了连续的ID值且与第一次执行时截然不同但是还缺少ID 和请留意标号及函数F的定义Point参数是传值到此函数的而一个Point也是通过值返回的同样地这两者都会调用到复制构造函数而其则忠实地创建了一个新实例且每个新实例都有一个唯一的ID因此当p通过值传递时会创建一个ID为的临时Point紧接着当副本通过值返回时又会创建一个ID为的副本而两个副本都是可丢弃的当程序结束时写入到文件中下一个可用的ID为而在程序下次运行时这就是第一个Point在分配空间时将用到的ID
解决方法
例中为Point类的修订版本非常明显每个实例现在必须包含一个额外的字段(在此为ID)用以保存ID在此选择的类型为int虽然标准C++允许其最小为位但在CLI环境中其至少为位如果以零开始那么在ID重复之前能表示亿个不同的实例当然也能以负亿开始那么能表示的范围又将扩展一倍倘若想要把ID字段再进行扩展可使用类型long long int那么至少能有位可以创建数不胜数的实例那么ID为unsigned行吗?如果它的值不会输出到它的父类之外是可以的请记住一点无符号整型与CLS不兼容(还可选择System::Decimal其可表示位)
例
using namespace System;
using namespace System::IO;
public ref class Point
{
int x;
int y;
/**/ int ID;
/**/ static int nextAvailableID;
/**/ static int GetNextAvailableID() { return nextAvailableID++; }
/**/ static bool traceID = false;
/**/ static String^ masterFileLocation;
/**/ static Point()
{
/*a*/ AppDomain^ appDom = AppDomain::CurrentDomain;
/*b*/ masterFileLocation = String::Concat(appDom>BaseDirectory \\PointIDtxt);
/*c*/ try {
/*d*/ StreamReader^ inStream = File::OpenText(masterFileLocation);
/*e*/ String^ s = inStream>ReadLine();
/*f*/ nextAvailableID = Int::Parse(s);
/*g*/ inStream>Close();
/*h*/ appDom>ProcessExit += gcnew
EventHandler(&Point::ProcessExitHandler);
}
/*i*/ catch (FileNotFoundException^ ioFNFEx)
{
//采取某些必要的措施
}
/*j*/ finally
{
appDom = nullptr;
}
}
/**/ static void ProcessExitHandler(Object^ sender EventArgs^ e)
{
/*a*/ StreamWriter^ outStream = File::CreateText(masterFileLocation);
/*b*/ outStream>WriteLine({} nextAvailableID);
/*c*/ outStream>Close();
}
public:
//
/**/ static property bool TraceID
{
bool get() { return traceID; }
void set(bool val) { traceID = val; }
}
// define instance constructors
Point()
{
/**/ ID = GetNextAvailableID();
X = ;
Y = ;
}
Point(int xor int yor)
{
/**/ ID = GetNextAvailableID();
X = xor;
Y = yor;
}
Point(Point% p) // copy constructor
{
/**/ ID = GetNextAvailableID();
X = pX;
Y = pY;
}
//
/**/ virtual int GetHashCode() override
{
//
}
virtual String^ ToString() override
{
/**/ if (traceID)
{
return String::Format([{}]({}{}) ID X Y);
}
else
{
return String::Format(({}{}) X Y);
}
}
};
一旦作为static标号至中定义的成员属于类而不属于任何实例而作为private它们只是一个实现的细节
使用这个类
C++/CLI在非本地类中引入了静态构造函数的概念它的类名声明为static如上例标号所示尽管一个静态构造函数是在类第一次使用之前被调用但使用意味着什么呢?一个引用类静态构造函数的执行是由类中对某个静态数据成员的第一次引用触发的
根据C++/CLI标准一个静态构造函数不应有一个ctor初始化过程(ctorinitializer)静态构造函数也不可以被继承且不能被直接调用如果一个类的初始化过程带有静态字段那么这些字段会在静态构造函数执行之前以声明的顺序被初始化
为静态构造函数生成的元数据总会标记为private而不管它们是否带有声明或暗指的访问指定符(但编译器会发出警告Accessibility on class constructor was ignored)在本文写作时至于一个带有给定访问指定符的静态构造函数是否应为private之外的问题仍在讨论之中因此访问指定符总是会被忽略
而一个没有显式指明静态构造函数的引用类它的行为就会像是有一个空的静态构造函数体一样
在上例标号a中利用AppDomain类为当前线程获取了应用程序域(Application domains)而根据CLI标准库应用程序域表现为System::AppDomain对象提供了隔离性卸载及托管代码执行时的安全边界检查多个应用程序域可运行于单个进程中但是也不存在应用程序域与线程的一对一关系可以同时有几个线程属于某一个应用程序域且同时某一个既定的线程也不会限制在某个单独的应用程序域中但无论何时一个线程只能在一个应用程序域中执行
用于追蹤在程序执行时下一个可用的ID的文本文件名为PointIDtxt与可执行程序位于同一目录中如标号b所示(Concat可同时用于一个Unicode宽字符串及普通窄字符串其会在编译时自动转换为宽字符串)在标号d中打开此文件并在标号e中读取输入的字符串在标号f中转换为一个整数接着在标号g中关闭此文件而try/catch块用于可能抛出的I/O异常
只读属性BaseDirectory与CurrentDomain是Microsoft对标准CLI库的扩展
在I/O中使用的类型如StreamReader与File存在于System::IO命名空间中
标号h注册了一个处理函数用于在程序快要结束时调用注意对一个类来说没有静态析构函数
Finally子句
C++/CLI支持对try/catch的一个扩展也就是finally子句位于它块内的代码总会被执行而不管对应的try块中是否产生了一个异常这就是说finally子句会在try块正常结束后执行或者说会在与try相联的catch块之后执行
在上例的标号j中finally子句只是简单地把appDom句柄设置为null值因此就不会再对AppDomain对象进行访问了但这种做法有点多余因为父类块退出时总会执行到这一行所以在此只是作为一个对此功能的简要介绍
事件处理
CLI支持事件的概念简单来说一个事件就是一个非本地类成员它可使一个对象或类提供通知机制标准CLI类System::AppDomain包含了几个这样的事件但Microsoft的扩展版本甚至包含了更多的事件比如说ProcessExit其在例的标号h中被引用
当一个特定的事件发生时与事件相联的函数会以它们之前相联的顺序被调用从最简单的形式来说一个事件只与一个函数发生联系而这也只是通过简单的赋值完成的也就是说包装了函数的代理被赋值给事件成员而从更一般的形式来说一个事件在不同时间通过 += 复合赋值操作符可与任意多个函数相联之所以在标号h中使用这个操作符是因为不知道事件是否已与事件处理程序相联如果已经相联又使用了简单的 = 赋值符那么这些函数将不再与此事件相联系
每个事件都有一个类型以ProcessExit来说类型为System::EventHandler其是一个用于包装分别接受两个参数System::Object^ 与System::EventArgs^ 函数的代理类型且有一个void返回类型而定义在标号中的ProcessExitHandler函数也正好具有同样的特征(参数类型)同时在标号h中把此函数注册为一个事件处理程序以便在进程退出的事件发生时调用当这个函数被调用时它会覆写此前的文本文件写入一个下次执行时可用的ID值而传递进来的参数会被忽略
代理
根据C++/CLI标准代理定义为一个从System::Delegate继承而来的类它可用于调用所有带有一组参数的代理实例的函数(注意与指向成员函数的指针不同一个代理实例能被绑定至任意类的成员只要函数类型与代理类型匹配就可所以代理非常适合于匿名调用)
而在本例中用到了一个定义在CLI库中的代理类型名为System::EventHandler然而使用关键字delegate也能定义自己的代理类型在标号h中就使用了gcnew创建了一个代理的实例由于被包装的函数为static而构造函数的调用也只给了一个参数所以指向成员函数ProcessExitHandler的指针其类型也必须与代理相匹配(要包装一个实例函数必须提供实例自身的句柄作为第一个参数)
对Point的其他修改
对TraceID属性的读取与写入定义在标号中而使用在标号中
三个构造函数(标号)全部会创建新的Point实例所以它们需要为ID分配一个唯一的值且其他的成员函数只会对现有的实例进行操作而不会修改任何ID值初始化只会在当一个对象创建时才会发生因此也需要一个新的ID而赋值操作发生在对象创建之后所以在此不需要新的ID
在标号中GetHashCode返回一个int其正是ID所需的类型同样这个函数也能返回一个值从而保证有一个唯一的哈希值(当然了如果ID的类型为unsigned或long long就需要把它缩减为一个int类型)
至于是否包含ID前缀全在ToString中完成见标号
Initonly字段
在非本地类中如果一个字段声明中带有initonly标识符其通常为一个在ctor初始化过程构造函数体或一个静态构造函数中的左值而在其他情况中其为一个右值(特别要说明一个静态的initonly字段只能被静态的构造函数所修改而一个实例initonly字段只能被实例构造函数所修改)除了当类第一次使用或一个实例被创建时之外都可以把这个字段当作只读类型例如某些工程数据类型有一张静态系统表在每次程序运行时其值都必须从一个文件中读出但之后就当作只读类型例就是这样一种情况
例
using namespace System;
public ref class EngineeringData
{
/**/ static initonly array<double>^ coefficients;
/**/ static EngineeringData()
{
int elementCount;
//找出需要多大的数组
// elementCount =
coefficients = gcnew array<double>(elementCount);
for (int i = ; i < elementCount; ++i)
{
// coefficients[i] =
}
}
public:
/**/ static property double Coeff[int] {
double get(int index) { return coefficients[index]; }
}
};
int main()
{
double d;
try {
/**/ d = EngineeringData::Coeff[];
}
catch (IndexOutOfRangeException^ ex)
{
//处理异常
}
}
保存了系数的静态数组在标号中声明为initonly在静态构造函数中打开了一个包含系数的文件在确定数目后分配了相应大小的数组并从文件中读取数值保存到数组中
与其让数组成为public或让程序员用下标来直接访问数组倒不如让数组隐藏在一个只读的命名索引属性之后(方括号表示了索引属性)在本例中是以逗号隔开的索引列表这意味着可以使用一个下标来索引到这个类如标号所示(与多维数组下标相似索引访问一个索引属性是使用了[]中的逗号分隔索引列表)
C++/CLI默认情况下还允许一个索引属性名作为一个关键字也就是说一个实例名可被直接索引而无须使用任何成员名然而这只对实例索引的属性可行所以在此不能使用同样地属性名为Coeff
一个initonly字段不是一个编译时命名常量因此它无须包含一个带有常量的初始化过程且initonly也不会限制是否带有一个标量
如果一个类包含了带有初始化过程的任意initonly字段它们会以声明的顺序在静态构造函数执行之前被初始化
那能把Point类中的nextAvailableID标为initonly吗?毕竟它只会在构造函数中被修改答案是不可以因为它是一个静态成员且它只能被静态构造函数所更新