C++/CLI不但支持基於堆棧的對象,同時也支持基於堆的對象;然而,如果想與其他基於CLI的語言(如C#、J#、Visual Basic)進行互操作的話,必須要清楚地知道,這些語言只支持基於堆的對象;當處於基於堆的對象環境中時,你與對象之間,永遠只有"一臂之遙",比方說,兩個給定的句柄h1與h2,只有在為這種句柄類型定義了相應的賦值操作符時,*h1 = *h2才會工作正常,而對C++/CLI之外的其他語言中的類型來說,情況可能就不是這樣了。同樣地,一個遵從CLS的機制需要創建對象的一份副本,這種機制被稱為"克隆"。
使用CLI庫中的Clone函數
請看例1中的代碼,其使用了類似於矢量的一個System::ArrayList類,插1是程序的輸出。
例1:
using namespace System;
using namespace System::Collections;
void PrintEntries(String^ s, ArrayList^ aList);
int main()
{
ArrayList^ al1 = gcnew ArrayList;
/*1*/ al1->Add("Red");
al1->Add("Blue");
al1->Add("Green");
al1->Add("Yellow");
/*2*/ PrintEntries("al1", al1);
/*3*/ ArrayList^ al2 = static_cast<ArrayList^>(al1->Clone());
/*4*/ PrintEntries("al2", al2);
/*5*/ al1->Remove("Blue");
al1->Add("Black");
al1->RemoveAt(0);
al1->Insert(0, "Brown");
/*6*/ PrintEntries("al1", al1);
/*7*/ PrintEntries("al2", al2);
}
void PrintEntries(String^ s, ArrayList^ aList)
{
Console::Write("{0}: ", s);
for each(Object^ o in aList)
{
Console::Write("\t{0}", o);
}
Console::WriteLine();
}
插1:程序輸出
al1: Red Blue Green Yellow
al2: Red Blue Green Yellow
al1: Brown Green Yellow Black
al2: Red Blue Green Yellow
ArrayList al1由4個代表不同顏色的字符串組成,通過在標記3中調用ArrayList::Clone函數,可以對此對象作一個完整的復制,所以,標記2與4表示的輸出完全相同。
接下來,從al1中移除了第二個元素,在末尾加入了一個新的元素,並修改了第一個元素的值。當把標記6與7表示的輸出進行一個對比時,你會發現,對al1所作的修改,完全不會影響到al2。在此需要說明的是,al2內部的引用,指向其自身元素的私有副本,而不是al1中的元素,這就是通常提到的"深拷貝",反之,只是簡單地把兩個ArrayList內部引用指向同一個值集(如al2=al1的賦值操作),這稱為"淺拷貝"。
也就是說,如果你希望復制所擁有的對象,應該參照庫函數Clone機制中的復制過程。
在類型中添加克隆
克隆的關鍵是實現System::ICloneable標准接口,其需要你定義一個調用Clone、不接受任何參數、並帶有一個System::Object^返回類型的函數,返回的句柄指向一個新的對象,這個對象是被調用對象的一個副本。請看例2:
例2:
public ref class Point : ICloneable
{
// ...
public:
virtual Object^ Clone()
{
return MemberwiseClone();
}
};
int main()
{
/*1*/ Point^ p1 = gcnew Point(3, 5);
/*2*/ Console::WriteLine("p1: {0}", p1);
/*3*/ Point^ p2 = static_cast<Point^>(p1->Clone());
/*4*/ p1->Move(9, 11);
/*5*/ Console::WriteLine("p1: {0}", p1);
/*6*/ Console::WriteLine("p2: {0}", p2);
}
以下是程序的輸出:
p1: (3,5)
p1: (9,11)
p2: (3,5)
在標記3中,通過調用Clone進行了復制,而因為此函數返回一個Object^類型的值(在此為一個Point的引用),在把它賦值給p2之前,必須轉換為一個Point^。(即便Point::Clone真的返回一個Point的句柄,也不能這樣聲明函數,因為不符合接口規范。)
在類型System::Object中定義了一個名為MemberwiseClone的函數,如下所示:
protected:
Object^ MemberwiseClone();
這個函數創建並返回對象的一份副本,而一般的用法是,對任意句柄x,以下的表達式都為真:
x->MemberwiseClone() != x
x->MemberwiseClone()->GetType() == x->GetType()
通常來說,復制一個對象必須創建對象的一個新實例,但同時也可能需要對內部數據結構進行復制,在此不需要調用任何的構造函數。
Object::MemberwiseClone執行一個詳細而精確的克隆操作。它創建類對象的一個新實例,並用源對象字段內容,初始化對應的所有字段,就好像在賦值;但是要注意的是,字段內容本身並沒有被克隆,所以,這個函數執行的是一個對象的"淺拷貝"。
在Point的實現中,使用了兩個實例變量,兩者均具有基本類型int。基本類型就是值類型,所以對一個Point的淺拷貝已經完全能滿足我們的需要,也就是通過調用基類對象的MemberwiseClone來完成的。
下面來看一個Circle類,其包含了一個指向Point的句柄(表示原始位置)和一個基本類型字段(表示radius半徑);見例3:
例3:
using namespace System;
public ref class Circle : ICloneable
{
Point^ origin;
float radius;
public:
property Point^ Origin
{
Point^ get() { return static_cast<Point^>(origin->Clone()); }
}
void SetOrigin(int x, int y)
{
origin->X = x;
origin->Y = y;
}
void SetOrigin(Point^ p)
{
SetOrigin(p->X, p->Y);
}
property double Radius
{
double get() { return radius; }
void set(double value) {
radius = static_cast<float>(value);
}
}
Circle()
{
origin = gcnew Point;
SetOrigin(0, 0);
Radius = 0.0;
}
Circle(int x, int y, double r)
{
origin = gcnew Point;
SetOrigin(x, y);
Radius = r;
}
Circle(Point^ p, double r)
{
origin = gcnew Point;
SetOrigin(p->X, p->Y);
Radius = r;
}
virtual String^ ToString() override
{
return String::Concat("{", Origin, ",", Radius, "}");
}
virtual Object^ Clone()
{
/*1*/ Circle^ c = static_cast<Circle^>(MemberwiseClone());
/*2*/ c->origin = static_cast<Point^>(origin->Clone());
/*3*/ return c;
}
};
Circle類中Origin屬性的get方法由Point::Clone來實現,如定義中所示,其返回一個Point中心點的Point副本。
在標記1中,調用了Object::MemberwiseClone以對Circle進行了一次淺拷貝,新Circle中的radius就與當前值一樣了,並且兩個Circle中的origin均引用同一個Point;因此,在標記2中,調用了Point::Clone以確保新Circle的origin引用為一個當前Point中心點的副本;最後,在標記3中,返回了這個新Circle的句柄。例4是測試這個類的程序:
例4:
int main()
{
/*1*/ Circle^ c1 = gcnew Circle(5, 9, 1.5);
/*2*/ Console::WriteLine("c1: {0}", c1);
/*3*/ Circle^ c2 = static_cast<Circle^>(c1->Clone());
/*4*/ Point^ p = c1->Origin;
/*5*/ Console::WriteLine(" p: {0}", p);
/*6*/ c1->SetOrigin(9, 11);
/*7*/ Console::WriteLine("c1: {0}", c1);
/*8*/ Console::WriteLine(" p: {0}", p);
/*9*/ Console::WriteLine("c2: {0}", c2);
}
克隆數組
請看例5,其擴展了泛型類Vector以支持克隆。因為所有的CLI數組類型均派生自System::Array,而其也實現了System::ICloneable接口,所以可以對一個數組的引用,簡單地調用Clone以復制數組中的元素,如例子中所示。當然,在轉換中也必須包括數組符號。
例5:
generic <typename T>
public ref class Vector
{
int length;
/*1*/ array<T>^ vector;
public:
virtual Object^ Clone()
{
Vector<T>^ v = static_cast<Vector<T>^>(MemberwiseClone());
v->vector = static_cast<array<T>^>(vector->Clone());
return v;
}
};
int main()
{
/*1*/ Vector<int>^ v1 = gcnew Vector<int>(5, 7);
/*2*/ Console::WriteLine("v1: {0}", v1);
/*3*/ Vector<int>^ v2 = static_cast<Vector<int>^>(v1->Clone());
/*4*/ Console::WriteLine("v2: {0}", v2);
/*5*/ v1[0] = 3;
/*6*/ v1[3] = 9;
/*7*/ v2[4] = 1;
/*8*/ Console::WriteLine("v1: {0}", v1);
/*9*/ Console::WriteLine("v2: {0}", v2);
}
克隆並派生類
到目前為止,所演示的方法對直接從Object^派生而來的類來說,都完全正確,然而,對從其他類派生而來的類來說,就不正確了。請看例6,因為是直接從System::Object派生而來,所以它的克隆方法正如前面所看到的那樣。Base被用作基類,而例7中的程序由其派生而來:
例6:
public ref class Base : ICloneable
{
array<int>^ bPair ;
public:
Base(int i, int j)
{
bPair = gcnew array<int>(2) {i, j};
}
void SetValue(int i, int j)
{
bPair[0] = i;
bPair[1] = j;
}
virtual String^ ToString() override
{
return String::Concat("[", bPair[0], ":", bPair[1], "]");
}
virtual Object^ Clone() override
{
Base^ b = static_cast<Base^>(MemberwiseClone());
b->bPair = static_cast<array<int>^>(bPair->Clone());
return b;
}
};
例7:
using namespace System;
public ref class Derived : Base, ICloneable
{
array<int>^ dPair;
public:
Derived(int bi, int bj, int i, int j) : Base(bi, bj)
{
dPair = gcnew array<int>(2) {i, j};
}
void SetValue(int bi, int bj, int i, int j)
{
Base::SetValue(bi, bj);
dPair[0] = i;
dPair[1] = j;
}
virtual String^ ToString() override
{
return String::Concat("[{", Base::ToString(), "}", dPair[0], ":", dPair[1], "]");
}
virtual Object^ Clone() override
{
// Derived^ d = static_cast<Derived^>(Base::MemberwiseClone());
Derived^ d = static_cast<Derived^>(Base::Clone());
d->dPair = static_cast<array<int>^>(dPair->Clone());
return d;
}
};
在Derived中顯示聲明實現ICloneable是多余的,因為Derived的基類已經這樣做了。而這個例子中唯一新的東西就是在Clone中調用Base::Clone,這取代了前一個對MemberwiseClone的調用(其已被注釋)。通過調用Base::Clone(其會調用基類的Clone,而它又最終調用Object::MemberwiseClone),這做就得到了一個基類對象的克隆。例8是測試程序。
例8:
int main()
{
Derived^ d1 = gcnew Derived(10, 20, 30, 40);
Console::WriteLine("d1 = {0}", d1);
Derived^ d2 = static_cast<Derived^>(d1->Clone());
Console::WriteLine("d2 = {0}", d2);
d1->Base::SetValue(5, 6);
Console::WriteLine("d1 = {0}", d1);
Console::WriteLine("d2 = {0}", d2);
}
在此,為什麼要讓Clone成為一個虛擬函數呢?因為,當調用Base::Clone時,必須確保調用了最恰當的實現,而虛擬函數查找,正是實現此的良方。
無構造的創建
一個對Clone的實現,不應調用任何其自身的類構造函數,對大多數類而言,這都不是問題,因為所有它們的構造函數只是初始化非靜態的數據成員,然而,如果一個構造函數初始化了任意的靜態數據成員或執行了其他的操作,這些操作在克隆期間就不會被完成,除非Clone本身執行了這些操作。在例9中,類Test包含了一個靜態計數器,其跟蹤類型對象被創建的數目;標記1與2執行了與構造函數同樣的操作。
例9:
public ref class Test : ICloneable
{
int data;
static int objectCount = 0;
public:
Test()
{
data = 0;
++objectCount;
}
Test(int value)
{
data = value;
++objectCount;
}
virtual String^ ToString() override
{
return String::Concat(data, ", ", objectCount);
}
virtual Object^ Clone()
{
/*1*/ Test^ copy = static_cast<Test^>(MemberwiseClone());
/*2*/ ++objectCount;
return copy;
}
};
int main()
{
/*3*/ Test^ t1 = gcnew Test;
Console::WriteLine("t1 using new: {0}", t1);
/*4*/ Test^ t2 = static_cast<Test^>(t1->Clone());
Console::WriteLine("t2 using Clone: {0}", t2);
/*5*/ Test^ t3 = gcnew Test(1);
Console::WriteLine("t3 using new: {0}", t3);
/*6*/ Test^ t4 = static_cast<Test^>(t3->Clone());
Console::WriteLine("t4 using Clone: {0}", t4);
}
}
以下是程序輸出:
t1 using new: 0, 1
t2 using Clone: 0, 2
t3 using new: 1, 3
t4 using Clone: 1, 4
由輸出可見,當一個對象被構造或被克隆時,對象計數都相應地增長了。