就某些類而言
當在程序中第一次使用時
最好能有一個初始化過程
當程序不再需要時
也最好能做一些收尾工作
這些都是非常好的類設計習慣
引出問題 如果有這樣一種情況
某種類型的每個實例都必須有其唯一的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嗎?畢竟它只會在構造函數中被修改答案是不可以因為它是一個靜態成員且它只能被靜態構造函數所更新
From:http://tw.wingwit.com/Article/program/net/201311/12046.html