與之相比,引用類型的實例--包括那些聲明在堆棧上的,是由垃圾回收器管理的,而值類型的實例卻不是。一般來說,一個值類較好的實現(xiàn)應(yīng)只有一些數(shù)據(jù)成員,而不需要繼承性,這樣,在函數(shù)傳遞及返回值、或是賦值操作時,不會帶來巨大的數(shù)據(jù)開銷。
值類初印像
請看例1中的Point類,可以通過替換ref為value,來把一個引用類變?yōu)橹殿;與引用類(ref)相似,值類(value)也是一個包含了空格的關(guān)鍵字。與大家想像的一樣,值類(value)與值結(jié)構(gòu)(value struct)之間唯一的區(qū)別就是,前者默認的可訪問性為private,而后者則為public。
例1:
using namespace System; public value class Point { int x; int y; public: //定義屬性X與 Y的讀寫實例 property int X { int get() { return x; } void set(int val) { x = val; } } property int Y { int get() { return y; } void set(int val) { y = val; } } //定義實例構(gòu)造函數(shù) Point(int xor, int yor) { X = xor; Y = yor; } void Move(int xor, int yor) { X = xor; Y = yor; } virtual bool Equals(Object^ obj) override { if (obj == nullptr) { return false; } if (GetType() == obj->GetType()) { Point^ p = static_cast<Point^>(obj); return (X == p->X) && (Y == p->Y); } return false; } static bool operator==(Point p1, Point p2) { return (p1.X == p2.X) && (p1.Y == p2.Y); } // static bool operator==(Point% p1, Point% p2) // { // return (p1.X == p2.X) && (p1.Y == p2.Y); // } // static bool operator==(Point& p1, Point& p2) // { // return (p1.X == p2.X) && (p1.Y == p2.Y); // } virtual int GetHashCode() override { return X ^ (Y << 1); } virtual String^ ToString() override { return String::Concat("(", X, ",", Y, ")"); } }; |
值類自動繼承自System::ValueType,而System::ValueType則繼承自System::Object,但是,這卻不能顯式地聲明。值類隱式表明了為"sealed",也就是說,它不能被作為一個基類,另外,為其類成員指定一個protected是沒有任何意義,并且也是不允許的。如果想顯式聲明一個值類(或引用類),可像如下所示:
value class X sealed {/*...*/}; |
請注意,此處沒有默認的構(gòu)造函數(shù)。對一個值類來說,CLI本身把類實例中所有字段的位都設(shè)置為零,所以,不能提供自己的默認構(gòu)造函數(shù);然而,零、false、nullptr對其他類型來說,也許并不是合適的默認值,因此,對某些特定類型來說,就要用引用類型來取代值類型了。(遵從C++/CLI的實現(xiàn)會將false與nullptr表示為位全部為零。)
值類的另一個限制是它們帶有一個默認的拷貝構(gòu)造函數(shù)和一個賦值操作符,兩者都會進行逐位復(fù)制,并不可被重載。
如果要實現(xiàn)Point類中的Equals函數(shù),相比引用類中的而言要簡單一些。請記住,我們正在重載定義System::Object中的這個版本,而其接受一個Object^,因為這種類型的參數(shù)很可能有一個nullptr值,在此,先可以省去檢查是否為自身比較這一步,而對引用類的Equals實現(xiàn)來說,這一步是必需的,因為可有多個句柄引用同一對象。但是話說回來,在目前的這個值類中,沒有兩個值的實例可表示同一個實例,兩個相同的值實例,只代表兩個Point有相同的坐標,但修改其中一者的x坐標,不會影響到另一者的相同值。
當(dāng)一個Point的實例傳遞到Equals時,作為值類型(其最終也都繼承自System::Object)而言,裝箱就發(fā)生了--也就是說,在垃圾回收堆上分配了一個Object的實例,而其包含了傳遞進來Point的一份副本。因為是創(chuàng)建了一個新的對象,所以只有一個句柄,也不會有相同的其他Point。
之前接受Point句柄的 == 操作符函數(shù),現(xiàn)在已經(jīng)精簡到一行,并且由接受句柄改為接受Point值,且用于選擇成員的指向操作符 -> 也被替換為點操作符。因為給定的值類型為sealed,所以與值類型參數(shù)Point唯一匹配的則為同類型的值了。同樣地,既無需檢查nullptr來確認是否為自身比較,也無需檢查傳遞進來的對象是否類型完全一致。
而之前用于追蹤引用的 == 操作符函數(shù)基本上無需太多改動,但刪除了檢測同一類型這一部分。然而,這兩個== 操作符函數(shù),最好只保留一個,以免在point1 == point2調(diào)用時引發(fā)歧義。(在聲明函數(shù)參數(shù)時,也可使用標準C++引用符&,而不是%,因為兩者可在本地類型與值類型之間互換。但由于這種類型的實例不存在于垃圾回收堆中,所以在垃圾回收期間不會改變它們的位置,因此也不需要對它們的位置進行追蹤。)
例2使用了值類中的大多數(shù)成員,最主要的是它包含了靜態(tài)Point類的實例,而這在引用類中是不可能完成的。事實上,不只是不能有一個引用類的靜態(tài)實例,甚至也不能有一個此類型的靜態(tài)句柄。
例2:
using namespace System; Point p1; static Point p2(3,4); int main() { static Point p3(4,7); Console::WriteLine("p2 is {0}", p2); Point% p4 = p3; Point p5 = p2; p5 = p2; Console::WriteLine("p1 == p2 is {0}", p1 == p2); Console::WriteLine("p1.Equals(p2) is {0}", p1.Equals(p2)); } p2 is (3,4) p1 == p2 is False p1.Equals(p2) is False |
在第一次調(diào)用Console::WriteLine時,用傳值的方式傳遞進一個Point,但是,這個函數(shù)卻指望著接受一個對象引用,在此,Point值被自動裝箱,并把裝箱后的對象引用傳遞給函數(shù)。
在定義中可看到,p5是由默認的拷貝構(gòu)造函數(shù)初始化,而接下來的一行代碼,默認的賦值操作符把p2逐位復(fù)制給p5。
引用類與值類的差異
如果我們在上述的Point引用類中加入一個ID號,用于跟蹤每個不同的Point引用對象,且再添加一個布爾類型的TraceID用于指明是否進行跟蹤;那么,把它改為值類之后,會有什么不同呢?
再次提醒,是不能為一個值類定義默認構(gòu)造函數(shù)、拷貝構(gòu)造函數(shù)及賦值操作符的,但不幸的是,這些都是我們ID解決方案中所需用到的。在引用類版本的默認構(gòu)造函數(shù)中,會將X與Y兩個坐標值、ID值都設(shè)置為零,并取得下一個ID賦給ID實例字段;反觀值類實現(xiàn)的版本,對以此方式構(gòu)建的每個新Point,都是由默認為零值的ID構(gòu)成,但是,我們卻想每個ID值為唯一。
另一個類似問題也是由缺少顯式的拷貝構(gòu)造函數(shù)造成的,在我們想要一個全新的對象時,值類的逐位復(fù)制卻造成新對象的ID與被拷貝對象的ID一樣。
另外,在賦值時,如果我們只設(shè)置即有Point的值,那么Point的ID不應(yīng)改變,也就是說,雖然任一或兩個坐標都可能改變,但它仍是同一Point對象,然而,逐位復(fù)制卻導(dǎo)致目標Point的ID被源對象ID覆蓋。
雖然此處沒有列出包含ID的Point類,但例3中的程序顯示了引用類與值類的差異所在。
例3:
using namespace System; int main() { Point::TraceID = true; Point p1, p2(3,7), p3(9,1), p4 = p2; Console::WriteLine("p1 = {0}", p1); Console::WriteLine("p2 = {0}", p2); Console::WriteLine("p3 = {0}", p3); Console::WriteLine("p4 = {0}", p4); p2 = p1; Console::WriteLine("p2 = {0}", p2); } |
第一次運行后,4個Point的輸出如下:
Point p1, p2(3,7), p3(9,1), p4 = p2; p1 = [0](0,0) p2 = [0](3,7) p3 = [1](9,1) p4 = [0](3,7) |
Point p1由默認構(gòu)造函數(shù)創(chuàng)建,它的ID為零,但卻恰好也是第一個Point的正確ID值,默認的坐標值也為零。而p2用到了自己編寫的構(gòu)造函數(shù),其分配了一個可用的ID,也就是零,這樣,我們有了兩個一樣的ID。
同樣地,p3得到了ID值1,接下來,把p2逐位復(fù)制給p4,p4的ID與p2相同。在執(zhí)行p2 = p1逐位復(fù)制之后,p1與p2兩個對象都有了相同的p1的ID。
程序第二次運行后,輸出如下:
p1 = [0](0,0) p2 = [2](3,7) p3 = [3](9,1) p4 = [2](3,7) p2 = [0](0,0) |
在此可看到,p1的ID值總為零。
顯而易見,引用類與值類是各有千秋,不是在每種場合,都可以調(diào)換使用的。
基本類型映射
遵照標準C++的精神,對CLI值類型的基本類型映射,都已經(jīng)全部在定義中實現(xiàn)了,就Microsoft Visual C++而言,映射關(guān)系如表1所示。
C++/CLI類型 | CLI值類型 |
bool | System::Boolean |
wchar_t | System::Char |
signed char | System::SByte |
unsigned char | System::Byte |
char | System::SByte或 System::Byte |
short int | System::Int16 |
unsigned short int | System::UInt16 |
int | System::Int32 |
unsigned int | System::UInt32 |
long long int | System::Int64 |
unsigned long long int | System::UInt64 |
float | System::Single |
double | System::Double |
表1:C++/CLI與CLI值類的映射關(guān)系
另外,還有一種值類型:System::Decimal,但沒有對應(yīng)的C++/CLI類型。
請看以下表達式,它們都涉及到訪問前述CLI值類型的靜態(tài)或?qū)嵗蓡T。
Int32::MaxValue Double::Parse("123.45e-1") 10.2f.ToString() (10 + 5.9).ToString() (100).ToString() 100 .ToString() |
因應(yīng)Visual C++的映射,10.2f的類型為float,其映射為System::Single,并調(diào)用了其ToString函數(shù);類似地,(10 + 5.9)類型為double,因此調(diào)用了System::Double的ToString。顯然,從語義的角度來看,帶有圓括號的100與其后帶有一個空格的100,這種寫法是多余的,但是,如果忽略它們,100與其后的句點將會解析為一個帶有標識符的double常量,這會導(dǎo)致語法錯誤。
復(fù)數(shù)問題
例4,演示了一個有著實部與虛部的復(fù)數(shù)的值類型。
例4:
using namespace System; public value class Complex { double real; double imag; public: static initonly Complex i; static Complex() { i = Complex(0.0, 1.0); } Complex(double real) { this->real = real; this->imag = 0.0; } Complex(double real, double imag) { this->real = real; this->imag = imag; } property double Real { double get() { return real; } void set(double value) { real = value; } } property double Imag { double get() { return imag; } void set(double value) { imag = value; } } static Complex operator+(Complex z1, Complex z2) { return Complex(z1.real + z2.real, z1.imag + z2.imag); } static Complex operator-(Complex z1, Complex z2) { return Complex(z1.real - z2.real, z1.imag - z2.imag); } String^ ToString() override { if (imag < 0.0) { return String::Format("({0} - {1}i)", real, -imag); } else if (1.0/imag == Double::NegativeInfinity) { return String::Format("({0} - 0.0i)", real); } else { return String::Format("({0} + {1}i)", real, +imag); } } }; |
CLI要求使用IEEE浮點表示法,這是一種比IEC 10559更正式的表示法,其中,零在single與double中表示為全部位為零。正因為此,所以可安全地使用CLI提供的默認構(gòu)造值。
程序中,定義了一個復(fù)數(shù)i,其表示-1的平方根,這樣,復(fù)數(shù)類型就可以提供具有此值的public只讀常量,而這是由一個public static成員及一個static構(gòu)造函數(shù)共同完成的。因為Complex在此不是一個基本類型,所以i不能成為一個只讀(readonly)成員,因為無論如何,這都需要用一個常量表達式來初始化它,但這種事是不存在的。所以,我們能做的,就是讓i成為initonly,并在static構(gòu)造函數(shù)中初始化它。例5是測試程序及輸出。
例5:
using namespace System; int main() { Complex c1; Complex c2(12.5); Complex c3(-1.23, -4.5); Complex c4 = c2 + c3; Complex c5 = c2 - c3; Console::WriteLine("c1: {0}", c1); Console::WriteLine("c2: {0}", c2); Console::WriteLine("c3: {0}", c3); Console::WriteLine("c4: {0}", c4); Console::WriteLine("c5: {0}", c5); Console::WriteLine("i: {0}", Complex::i); Console::WriteLine("c3.Real: {0}", c3.Real); Console::WriteLine("c3.Imag: {0}", c3.Imag); } c1: (0 + 0i) c2: (12.5 + 0i) c3: (-1.23 - 4.5i) c4: (11.27 - 4.5i) c5: (13.73 + 4.5i) i: (0 + 1i) c3.Real: -1.23 c3.Imag: -4.5 |
一些其他事項
注意,一個值類型不應(yīng)包含:
·類型為本地C++數(shù)組、本地類類型或位字段的數(shù)據(jù)成員
·包含局部類的成員函數(shù)
·為friend的成員
·析構(gòu)函數(shù)
一個傳值、傳址、傳引用、或追蹤引用的函數(shù),可傳遞進或返回一個值類。
在引用類T的實例構(gòu)造函數(shù)或成員函數(shù)中,this的類型為"指向T的句柄",然而,對值類型而言,this為interior_ptr<T>。
像Point與Complex這樣的簡單值類型實例是完全自我包含的--但卻不是必須的,舉例來說,與引用類型相似,一個值類型也能包含指向本地堆的指針及垃圾回收堆中對象的句柄。在這種情況下,清理釋放值類型自身所占用的內(nèi)存可不是一件簡單的事情,因為每種類型的數(shù)據(jù)成員在超出作用域時,都需要進行清理。