实例解析C++/CLI之静态构造函数

来源:互联网 发布:nba数据排名 编辑:程序博客网 时间:2024/06/13 11:58
实例解析C++/CLI之静态构造函数
 
 
         就某些类而言,当在程序中第一次使用时,最好能有一个初始化过程;当程序不再需要时,也最好能做一些收尾工作,这些都是非常好的类设计习惯。
 
 
         引出问题
         如果有这样一种情况,某种类型的每个实例都必须有其唯一的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,它们只是一个实现的细节。
 
 
         使用这个类
         C++/CLI在非本地类中,引入了静态构造函数的概念,它的类名声明为static,如上例标号6所示。尽管一个静态构造函数是在类第一次使用之前被调用,但“使用”意味着什么呢?一个引用类静态构造函数的执行,是由类中对某个静态数据成员的第一次引用触发的。
 
         根据C++/CLI标准:一个静态构造函数不应有一个ctor初始化过程(ctor-initializer),静态构造函数也不可以被继承,且不能被直接调用。如果一个类的初始化过程带有静态字段,那么这些字段会在静态构造函数执行之前,以声明的顺序被初始化。
         为静态构造函数生成的元数据总会标记为private,而不管它们是否带有声明或暗指的访问指定符。(但编译器会发出警告:“Accessibility on class constructor was ignored”)。在本文写作时,至于一个带有给定访问指定符的静态构造函数,是否应为private之外的问题,仍在讨论之中,因此,访问指定符总是会被忽略。
         而一个没有显式指明静态构造函数的引用类,它的行为,就会像是有一个空的静态构造函数体一样。
 
         在上例标号6a中,利用AppDomain类,为当前线程获取了应用程序域(Application domains)。而根据CLI标准库:应用程序域表现为System::AppDomain对象,提供了隔离性、卸载及托管代码执行时的安全边界检查。多个应用程序域可运行于单个进程中,但是,也不存在应用程序域与线程的一对一关系,可以同时有几个线程属于某一个应用程序域,且同时某一个既定的线程也不会限制在某个单独的应用程序域中,但无论何时,一个线程只能在一个应用程序域中执行。
 
         用于追踪在程序执行时下一个可用的ID的文本文件名为“PointID.txt”,与可执行程序位于同一目录中,如标号6b所示。(Concat可同时用于一个Unicode宽字符串及普通窄字符串,其会在编译时自动转换为宽字符串。)在标号6d中打开此文件,并在标号6e中读取,输入的字符串在标号6f中转换为一个整数,接着,在标号6g中关闭此文件。而try/catch块用于可能抛出的I/O异常。
         只读属性BaseDirectory与CurrentDomain是Microsoft对标准CLI库的扩展。
         在I/O中使用的类型,如StreamReader与File,存在于System::IO命名空间中。
         标号6h注册了一个处理函数,用于在程序快要结束时调用。注意,对一个类来说,没有静态析构函数。
 
 
         Finally子句
         C++/CLI支持对try/catch的一个扩展,也就是finally子句,位于它块内的代码总会被执行,而不管对应的try块中是否产生了一个异常。这就是说,finally子句会在try块正常结束后执行,或者说,会在与try相联的catch块之后执行。
         在上例的标号6j中,finally子句只是简单地把appDom句柄设置为null值,因此,就不会再对AppDomain对象进行访问了。但这种做法有点多余,因为父类块退出时,总会执行到这一行,所以,在此只是作为一个对此功能的简要介绍。
 
 
         事件处理
         CLI支持事件的概念,简单来说,一个事件就是一个非本地类成员,它可使一个对象或类提供通知机制。标准CLI类System::AppDomain包含了几个这样的事件,但Microsoft的扩展版本甚至包含了更多的事件,比如说ProcessExit,其在例2的标号6h中被引用。
         当一个特定的事件发生时,与事件相联的函数会以它们之前相联的顺序被调用,从最简单的形式来说,一个事件只与一个函数发生联系,而这也只是通过简单的赋值完成的,也就是说,包装了函数的代理被赋值给事件成员。而从更一般的形式来说,一个事件在不同时间,通过 += 复合赋值操作符,可与任意多个函数相联。之所以在标号6h中使用这个操作符,是因为不知道事件是否已与事件处理程序相联,如果已经相联,又使用了简单的 = 赋值符,那么这些函数将不再与此事件相联系。
         每个事件都有一个类型,以ProcessExit来说,类型为System::EventHandler,其是一个用于包装分别接受两个参数System::Object^ 与System::EventArgs^ 函数的代理类型,且有一个void返回类型。而定义在标号7中的ProcessExitHandler函数,也正好具有同样的特征(参数类型)。同时,在标号6h中,把此函数注册为一个事件处理程序,以便在进程退出的事件发生时调用。当这个函数被调用时,它会覆写此前的文本文件,写入一个下次执行时可用的ID值,而传递进来的参数会被忽略。
 
 
         代理
         根据C++/CLI标准:代理定义为一个从System::Delegate继承而来的类,它可用于调用所有带有一组参数的代理实例的函数。(注意,与指向成员函数的指针不同,一个代理实例能被绑定至任意类的成员,只要函数类型与代理类型匹配就可,所以,代理非常适合于“匿名”调用。)
         而在本例中,用到了一个定义在CLI库中的代理类型,名为System::EventHandler。然而,使用关键字delegate,也能定义自己的代理类型。在标号6h中,就使用了gcnew创建了一个代理的实例。由于被包装的函数为static,而构造函数的调用也只给了一个参数,所以,指向成员函数ProcessExitHandler的指针,其类型也必须与代理相匹配。(要包装一个实例函数,必须提供实例自身的句柄作为第一个参数。)
 
 
         对Point的其他修改
         对TraceID属性的读取与写入定义在标号8中,而使用在标号12中。
         三个构造函数(标号9、10、11)全部会创建新的Point实例,所以它们需要为ID分配一个唯一的值,且其他的成员函数只会对现有的实例进行操作,而不会修改任何ID值。初始化只会在当一个对象创建时才会发生,因此也需要一个新的ID,而赋值操作发生在对象创建之后,所以在此不需要新的ID。
         在标号12中,GetHashCode返回一个int,其正是ID所需的类型。同样,这个函数也能返回一个值,从而保证有一个唯一的哈希值。(当然了,如果ID的类型为unsigned或long long,就需要把它缩减为一个int类型。)
         至于是否包含ID前缀,全在ToString中完成,见标号13。
 
 
         Initonly字段
         在非本地类中,如果一个字段声明中带有initonly标识符,其通常为一个在ctor初始化过程、构造函数体、或一个静态构造函数中的左值,而在其他情况中,其为一个右值。(特别要说明,一个静态的initonly字段只能被静态的构造函数所修改,而一个实例initonly字段只能被实例构造函数所修改。)除了当类第一次使用,或一个实例被创建时之外,都可以把这个字段当作只读类型,例如,某些工程数据类型有一张静态系统表,在每次程序运行时,其值都必须从一个文件中读出,但之后,就当作只读类型,例3就是这样一种情况。
 
例3:
using namespace System;
 
public ref class EngineeringData
{
/*1*/   static initonly array<double>^ coefficients;
/*2*/   static EngineeringData()
    {
        int elementCount;
        //找出需要多大的数组
        // elementCount = ...
        coefficients = gcnew array<double>(elementCount);
        for (int i = 0; i < elementCount; ++i)
        {
            // coefficients[i] = ...
        }
   }
public:
/*3*/   static property double Coeff[int] {
        double get(int index) { return coefficients[index]; }
    }
};
int main()
{   double d;
    try {
/*4*/       d = EngineeringData::Coeff[2];
    }
    catch (IndexOutOfRangeException^ ex)
    {
        //处理异常
    }
}
 
         保存了系数的静态数组在标号1中声明为initonly,在静态构造函数中,打开了一个包含系数的文件,在确定数目后分配了相应大小的数组,并从文件中读取数值,保存到数组中。
         与其让数组成为public或让程序员用下标来直接访问数组,倒不如让数组隐藏在一个只读的命名索引属性之后。(方括号表示了索引属性。)在本例中,是以逗号隔开的索引列表,这意味着,可以使用一个下标来索引到这个类,如标号4所示。(与多维数组下标相似,索引访问一个索引属性是使用了[]中的逗号分隔索引列表。)
         C++/CLI默认情况下还允许一个索引属性名作为一个关键字,也就是说,一个实例名可被直接索引,而无须使用任何成员名。然而,这只对实例索引的属性可行,所以在此不能使用。同样地,属性名为Coeff。
 
         一个initonly字段不是一个编译时命名常量,因此,它无须包含一个带有常量的初始化过程,且initonly也不会限制是否带有一个标量。
         如果一个类包含了带有初始化过程的任意initonly字段,它们会以声明的顺序,在静态构造函数执行之前被初始化。
         那能把Point类中的nextAvailableID标为initonly吗?毕竟,它只会在构造函数中被修改,答案是不可以,因为它是一个静态成员,且它只能被静态构造函数所更新。
 
 
原创粉丝点击