就某些类而言,当在程序中第一次使用时,最好能有一个初始化过程;当程序不再需要时,也最好能做一些收尾工作,这些都是非常好的类设计习惯。
引出问题
如果有这样一种情况,某种类型的每个实例都必须有其唯一的ID,比如说某种交易类型,这些ID可用于在处理过程中追踪每笔交易,或之后用于审计员查看数据文件;为讨论方便,此处的ID为从0起始的有符号整型数。
如果把一个nextID值保存在内存中,并在每个新实例构造时,把它递增1,这无疑是一个不错的想法,但是,为使在程序连续的执行过程中保持ID值的唯一,就需要在每次程序结束时保存此值,并在下次程序开始运行时恢复这个值,但在标准C++中,是没办法来达到这个目的的,实际上,使用标准CLI库也同样没办法完成。然而,在CLI的。NET实现中有几个扩展库,它们却可以完成这个任务。
问题重现
这回又用到了Point类,因为带有唯一ID的点很适合此主题。例1中的程序输出在代码之后:
例1:
using namespace System; Point F(Point p) { return p; }int main() { /*1*/ Point::TraceID = true; /*2*/ Point^ hp1 = gcnew Point; Console::WriteLine("hp1: {0}", hp1); /*3*/ hp1->Move(6,7); Console::WriteLine("hp1: {0}", hp1); /*4*/ Point^ hp2 = gcnew Point(3,4); Console::WriteLine("hp2: {0}", hp2); /*5*/ Point p1, p2(-1,-2); Console::WriteLine("p1: {0}, p2: {1}", %p1, %p2); /*6*/ p1 = F(p2); Console::WriteLine("p1: {0}", %p1); }
|
输出:
hp1: [0](0,0) hp1: [0](6,7) hp2: [1](3,4) p1: [2](0,0), p2: [3](-1,-2) p1: [2](-1,-2) |
在程序开始运行时,从一个文本文件中读取下一个可用的ID值,并用它来初始化一个Point类中的私有静态(private static)字段。最开始,这个文件包含的值为零。
基于公共静态布尔属性TraceID的值,Point中ToString函数生成的字符串可有选择地包含Point的ID,并以 [id] 的形式作为一个前缀。如果此属性值为true,就包含ID前缀;否则,就不包含。默认情况下,这个属性值被设为false,因此,在标号1中我们把它设为true.
在标号2中,使用默认构造函数为Point分配了内存空间,并显示它的ID为0及值为(0,0)。在标号3中,通过Move函数修改了Point的x与y坐标值,但这不会修改Point的ID,毕竟,它仍是同一个实例——只不过用了不同的值。接着,在标号4中,使用了接受两个参数的构造函数为另一个Point分配了内存空间,并显示它的ID为1及值为(3,4)。
在标号5中创建了两个基于堆栈的实例,并显示出它们的ID及值。在第三个及第四个Point创建时,它们的ID分别为2和3.
在标号6中,p1被赋于了一个新值,然而,p1仍是它之前的同一个Point,所以它的ID没有改变。
第二次运行程序时,输出如下:
hp1: [6](0,0) hp1: [6](6,7) hp2: [7](3,4) p1: [8](0,0), p2: [9](-1,-2) p1: [8](-1,-2) |
如上所示,4个新实例都被赋于了连续的ID值,且与第一次执行时截然不同,但是,还缺少ID 4和5.请留意标号6及函数F的定义,Point参数是传值到此函数的,而一个Point也是通过值返回的。同样地,这两者都会调用到复制构造函数,而其则"忠实"地创建了一个新实例,且每个新实例都有一个唯一的ID.因此,当p2通过值传递时,会创建一个ID为4的临时Point,紧接着,当副本通过值返回时,又会创建一个ID为5的副本,而两个副本都是可丢弃的。当程序结束时,写入到文件中下一个可用的ID为6,而在程序下次运行时,这就是第一个Point在分配空间时将用到的ID.
解决方法
例2中为Point类的修订版本,非常明显,每个实例现在必须包含一个额外的字段(在此为ID),用以保存ID,在此选择的类型为int,虽然标准C++允许其最小为16位,但在CLI环境中,其至少为32位。如果以零开始,那么在ID重复之前,能表示20亿个不同的实例;当然,也能以负20亿开始,那么能表示的范围又将扩展一倍;倘若想要把ID字段再进行扩展,可使用类型long long int,那么至少能有64位,可以创建数不胜数的实例。那么ID为unsigned行吗?如果它的值不会输出到它的父类之外,是可以的,请记住一点,无符号整型与CLS不兼容。(还可选择System::Decimal,其可表示128位。)
例2:
using namespace System; using namespace System::IO; public ref class Point { int x; int y; /*1*/ int ID; /*2*/ static int nextAvailableID; /*3*/ static int GetNextAvailableID() { return nextAvailableID++; } /*4*/ static bool traceID = false; /*5*/ static String^ masterFileLocation; /*6*/ static Point() { /*6a*/ AppDomain^ appDom = AppDomain::CurrentDomain; /*6b*/ masterFileLocation = String::Concat(appDom->BaseDirectory, "\\PointID.txt"); /*6c*/ try { /*6d*/ StreamReader^ inStream = File::OpenText(masterFileLocation); /*6e*/ String^ s = inStream->ReadLine(); /*6f*/ nextAvailableID = Int32::Parse(s); /*6g*/ inStream->Close(); /*6h*/ appDom->ProcessExit += gcnew EventHandler(&Point::ProcessExitHandler); } /*6i*/ catch (FileNotFoundException^ ioFNFEx) { //采取某些必要的措施 } /*6j*/ finally { appDom = nullptr; } } /*7*/ static void ProcessExitHandler(Object^ sender, EventArgs^ e) { /*7a*/ StreamWriter^ outStream = File::CreateText(masterFileLocation); /*7b*/ outStream->WriteLine("{0}", nextAvailableID); /*7c*/ outStream->Close(); } public: // ... /*8*/ static property bool TraceID { bool get() { return traceID; } void set(bool val) { traceID = val; } } // define instance constructors Point() { /*9*/ ID = GetNextAvailableID(); X = 0; Y = 0; } Point(int xor, int yor) { /*10*/ ID = GetNextAvailableID(); X = xor; Y = yor; } Point(Point% p) // copy constructor { /*11*/ ID = GetNextAvailableID(); X = p.X; Y = p.Y; } // ... /*12*/ virtual int GetHashCode() override { // ... } virtual String^ ToString() override { /*13*/ if (traceID) { return String::Format("[{0}]({1},{2})", ID, X, Y); } else { return String::Format("({0},{1})", X, Y); } } };
|
一旦作为static,标号2至5中定义的成员属于类,而不属于任何实例;而作为private,它们只是一个实现的细节。