副标题#e#
就某些类而言,当在措施中第一次利用时,最好能有一个初始化进程;当措施不再需要时,也最好能做一些收尾事情,这些都长短常好的类设计习惯。
引出问题
假如有这样一种环境,某种范例的每个实例都必需有其独一的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。
#p#副标题#e#
办理要领
例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,它们只是一个实现的细节。
利用这个类
#p#分页标题#e#
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子句
#p#分页标题#e#
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:
#p#分页标题#e#
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吗?究竟,它只会在结构函数中被修改,谜底是不行以,因为它是一个静态成员,且它只能被静态结构函数所更新。