C++的營養(yǎng)
莫華楓
動(dòng)物都會(huì)攝取食物,吸收其中的營養(yǎng),用于自身生長和活動(dòng)。然而,并非食物中所有的物質(zhì)都能為動(dòng)物所吸收。那些無法消化的物質(zhì),通過消化道的另一頭(某些動(dòng)
物消化道只有一頭)排出體外。不過,一種動(dòng)物無法消化的排泄物,是另一種動(dòng)物(生物)的食物,后者可以從中攝取所需的營養(yǎng)。
一門編程語言,對(duì)于程序員而言,如同食物那樣,包含著所需的養(yǎng)分。當(dāng)然也包含著無法消化的東西。不同的是,隨著程序員不斷成長,會(huì)逐步消化過去無法消化的那些東西。
C++可以看作一種成分復(fù)雜的食物,對(duì)于多數(shù)程序員而言,是無法完全消化的。正因?yàn)槿绱耍芏喑绦騿T認(rèn)為C++太難以消化,不應(yīng)該去吃它。但是,C++的
營養(yǎng)不可謂不豐富,就此舍棄,而不加利用,則是莫大的罪過。好在食物可以通過加工,變得易于吸收,比如說發(fā)酵。鑒于程序員們的消化能力的差異,也為了讓C
++的營養(yǎng)能夠造福他人,我就暫且扮演一回酵母菌,把C++的某些營養(yǎng)單獨(dú)提取出來,并加以分解,讓那些消化能力不太強(qiáng)的程序員也能享受它的美味。:)
(為了讓這些營養(yǎng)便于消化,我將會(huì)用C#做一些案例。選擇C#的原因很簡(jiǎn)單,因?yàn)槲沂煜ぁ?))
RAII
RAII,好古怪的營養(yǎng)?。∷娜Q應(yīng)該是“Resource Acquire Is Initial”。這是C++創(chuàng)始人Bjarne
Stroustrup發(fā)明的詞匯,比較令人費(fèi)解。說起來,RAII的含義倒也不算復(fù)雜。用白話說就是:在類的構(gòu)造函數(shù)中分配資源,在析構(gòu)函數(shù)中釋放資源。
這樣,當(dāng)一個(gè)對(duì)象創(chuàng)建的時(shí)候,構(gòu)造函數(shù)會(huì)自動(dòng)地被調(diào)用;而當(dāng)這個(gè)對(duì)象被釋放的時(shí)候,析構(gòu)函數(shù)也會(huì)被自動(dòng)調(diào)用。于是乎,一個(gè)對(duì)象的生命期結(jié)束后將會(huì)不再占用
資源,資源的使用是安全可靠的。
下面便是在C++中實(shí)現(xiàn)RAII的典型代碼:
class file
{
public:
file(string const& name) {
m_fileHandle=open_file(name.cstr());
}
~file() {
close_file(m_fileHandle);
}
...
private:
handle m_fileHandle;
}
很典型的“在構(gòu)造函數(shù)里獲取,在析構(gòu)函數(shù)里釋放”。如果我寫下代碼:

void fun1() ...{
file myfile("my.txt");
... //操作文件
} //此處銷毀對(duì)象,調(diào)用析構(gòu)函數(shù),釋放資源
當(dāng)函數(shù)結(jié)束時(shí),局部對(duì)象myfile的生命周期也結(jié)束了,析構(gòu)函數(shù)便會(huì)被調(diào)用,資源會(huì)得到釋放。而且,如果函數(shù)中的代碼拋出異常,那么析構(gòu)函數(shù)也會(huì)被調(diào)用,資源同樣會(huì)得到釋放。所以,在RAII下,不僅僅資源安全,也是異常安全的。
但是,在如下的代碼中,資源不是安全的,盡管我們實(shí)現(xiàn)了RAII:

void fun2() ...{
file pfile=new file("my.txt");
... //操作文件
}
因?yàn)槲覀冊(cè)诙焉蟿?chuàng)建了一個(gè)對(duì)象(通過new),但是卻沒有釋放它。我們必須運(yùn)用delete操作符顯式地加以釋放:

void fun3() ...{
file pfile=new file("my.txt");
... //操作文件
delete pfile;
}
否則,非但對(duì)象中的資源得不到釋放,連對(duì)象本身的內(nèi)存也得不到回收。(將來,C++的標(biāo)準(zhǔn)中將會(huì)引入GC(垃圾收集),但正如下面分析的那樣,GC依然無法確保資源的安全)。
現(xiàn)在,在fun3(),資源是安全的,但卻不是異常安全的。因?yàn)橐坏┖瘮?shù)中拋出異常,那么delete pfile;這句代碼將沒有機(jī)會(huì)被執(zhí)行。C++領(lǐng)域的諸位大牛們告誡我們:如果想要在沒有GC的情況下確保資源安全和異常安全,那么請(qǐng)使用智能指針:

void fun4() ...{
shared_ptr<file> spfile(new file("my.txt"));
... //操作文件
} //此處,spfile結(jié)束生命周期的時(shí)候,會(huì)釋放(delete)對(duì)象
那么,智能指針又是怎么做到的呢?下面的代碼告訴你其中的把戲(關(guān)于智能指針的更進(jìn)一步的內(nèi)容,請(qǐng)參考std::auto_ptr,boost或tr1的智能指針):
template<typename T>
class smart_ptr

...{
public:

smart_ptr(T* p):m_ptr(p) ...{}

~smart_ptr() ...{ delete m_ptr; }
...
private:
T* m_ptr;
}
沒錯(cuò),還是RAII。也就是說,智能指針通過RAII來確保內(nèi)存資源的安全,也間接地使得對(duì)象上的RAII得到實(shí)施。不過,這里的RAII并不是十分嚴(yán)
格:對(duì)象(所占的內(nèi)存也是資源)的創(chuàng)建(資源獲取)是在構(gòu)造函數(shù)之外進(jìn)行的。廣義上,我們也把它劃歸RAII范疇。但是,Matthew
Wilson在《Imperfect C++》一書中,將其獨(dú)立出來,稱其為RRID(Resource Release Is
Destruction)。RRID的實(shí)施需要在類的開發(fā)者和使用者之間建立契約,采用相同的方法獲取和釋放資源。比如,如果在shared_ptr構(gòu)造
時(shí)使用malloc(),便會(huì)出現(xiàn)問題,因?yàn)閟hared_ptr是通過delete釋放對(duì)象的。
對(duì)于內(nèi)置了GC的語言,資源管理相對(duì)簡(jiǎn)單。不過,事情并非總是這樣。下面的C#代碼摘自MSDN Library的C#編程指南,我略微改造了一下:
static void CodeWithoutCleanup()

...{
System.IO.FileStream file = null;
System.IO.FileInfo fileInfo = new System.IO.FileInfo("C:\file.txt");
file = fileInfo.OpenWrite();
file.WriteByte(0xF);
}
那么資源會(huì)不會(huì)泄漏呢?這取決于對(duì)象的實(shí)現(xiàn)。如果通過OpenWrite()獲得的FileStream對(duì)象,在析構(gòu)函數(shù)中執(zhí)行了文件的釋放操作,那么資
源最終不會(huì)泄露。因?yàn)镚C最終在執(zhí)行GC操作的時(shí)候,會(huì)調(diào)用Finalize()函數(shù)(C#類的析構(gòu)函數(shù)會(huì)隱式地轉(zhuǎn)換成Finalize()函數(shù)的重
載)。這是由于C#使用了引用語義(嚴(yán)格地講,是對(duì)引用類型使用引用語義),一個(gè)對(duì)象實(shí)際上不是對(duì)象本身,而是對(duì)象的引用。如同C++中的那樣,引用在離
開作用域時(shí),是不會(huì)釋放對(duì)象的。否則,便無法將一個(gè)對(duì)象直接傳遞到函數(shù)之外。在這種情況下,如果沒有顯式地調(diào)用Close()之類的操作,資源將不會(huì)得到
立刻釋放。但是像文件、鎖、數(shù)據(jù)庫鏈接之類屬于重要或稀缺的資源,如果等到GC執(zhí)行回收,會(huì)造成資源不足。更有甚者,會(huì)造成代碼執(zhí)行上的問題。我曾經(jīng)遇到
過這樣一件事:我執(zhí)行了一個(gè)sql操作,獲得一個(gè)結(jié)果集,然后執(zhí)行下一個(gè)sql,結(jié)果無法執(zhí)行。這是因?yàn)槲沂褂玫腟QL Server
2000不允許在一個(gè)數(shù)據(jù)連接上同時(shí)打開兩個(gè)結(jié)果集(很多數(shù)據(jù)庫引擎都是這樣)。第一個(gè)結(jié)果集用完后沒有立刻釋放,而GC操作則尚未啟動(dòng),于是便造成在一
個(gè)未關(guān)閉結(jié)果集的數(shù)據(jù)連接上無法執(zhí)行新的sql的問題。
所以,只要涉及了內(nèi)存以外的資源,應(yīng)當(dāng)盡快釋放。(當(dāng)然,如果內(nèi)存能夠盡快釋放,就更好了)。對(duì)于上述CodeWithoutCleanup()函數(shù),應(yīng)當(dāng)在最后調(diào)用file對(duì)象上的Close()函數(shù),以便釋放文件:
static void CodeWithoutCleanup()

...{
System.IO.FileStream file = null;
System.IO.FileInfo fileInfo = new System.IO.FileInfo("C:\file.txt");
file = fileInfo.OpenWrite();
file.WriteByte(0xF);
file.Close();
}
現(xiàn)在,這個(gè)函數(shù)是
嚴(yán)格資源安全的,但卻不是
嚴(yán)格異常安全的。如果在文件的操作中拋出異常,Close()成員將得不到調(diào)用。此時(shí),文件也將無法及時(shí)關(guān)閉,直到GC完成。為此,需要對(duì)異常作出處理:
static void CodeWithCleanup()

...{
System.IO.FileStream file = null;
System.IO.FileInfo fileInfo = null;
try

...{
fileInfo = new System.IO.FileInfo("C:\file.txt");
file = fileInfo.OpenWrite();
file.WriteByte(0xF);
}
catch(System.Exception e)

...{
System.Console.WriteLine(e.Message);
}
finally

...{
if (file != null)

...{
file.Close();
}
}
}
try-catch-finally是處理這種情況的標(biāo)準(zhǔn)語句。但是,相比前面的C++代碼fun1()和fun4()繁瑣很多。這都是沒有RAII的后果啊。下面,我們就來看看,如何在C#整出RAII來。
一個(gè)有效的RAII應(yīng)當(dāng)包含兩個(gè)部分:構(gòu)造/析構(gòu)函數(shù)的資源獲取/釋放和確定性的析構(gòu)函數(shù)調(diào)用。前者在C#中不成問題,C#有構(gòu)造函數(shù)和析構(gòu)函數(shù)。不過,
C#的構(gòu)造函數(shù)和析構(gòu)函數(shù)是不能用于RAII的,原因一會(huì)兒會(huì)看到。正確的做法是讓一個(gè)類實(shí)現(xiàn)IDisposable接口,在IDisposable::
Dispose()函數(shù)中釋放資源:
class RAIIFile : IDisposable

...{

public RAIIFile(string fn) ...{
System.IO.FileInfo fileInfo = new System.IO.FileInfo(fn);
file = fileInfo.OpenWrite();
}


public void Dispose() ...{
file.Close();
}

private System.IO.FileStream file = null;
}
下一步,需要確保文件在退出作用域,或發(fā)生異常時(shí)被確定性地釋放。這項(xiàng)工作需要通過C#的using語句實(shí)現(xiàn):
static void CodeWithRAII()

...{
using(RAIIFile file=new RAIIFile("C:\file.txt"))

...{
... //操作文件
} //文件釋放
}
一旦離開using的作用域,file.Dispose()將被調(diào)用,文件便會(huì)得到釋放,即便拋出異常,亦是如此。相比CodeWithCleanup
()中那坨雜亂繁復(fù)的代碼,CodeWithRAII()簡(jiǎn)直可以算作賞心悅目。更重要的是,代碼的簡(jiǎn)潔和規(guī)則將會(huì)大幅減少出錯(cuò)可能性。值得注意的是
using語句只能作用于實(shí)現(xiàn)IDisposable接口的類,即便實(shí)現(xiàn)了析構(gòu)函數(shù)也不行。所以對(duì)于需要得到RAII的類,必須實(shí)現(xiàn)
IDisposable。通常,凡是涉及到資源的類,都應(yīng)該實(shí)現(xiàn)這個(gè)接口,便于日后使用。實(shí)際上,.net庫中的很多與非內(nèi)存資源有關(guān)的類,都實(shí)現(xiàn)了
IDisposable,都可以利用using直接實(shí)現(xiàn)RAII。
但是,還有一個(gè)問題是using無法解決的,就是如何維持類的成員函數(shù)的RAII。我們希望一個(gè)類的成員對(duì)象在該類實(shí)例創(chuàng)建的時(shí)候獲取資源,而在其銷毀的時(shí)候釋放資源:
class X

...{
public:

X():m_file("c:\file.txt") ...{}
private:
File m_file; //在X的實(shí)例析構(gòu)時(shí)調(diào)用File::~File(),釋放資源。
}
但是在C#中無法實(shí)現(xiàn)。由于uing中實(shí)例化的對(duì)象在離開using域的時(shí)候便釋放了,無法在構(gòu)造函數(shù)中使用:
class X

...{

public X() ...{
using(m_file=new RAIIFile("C:\file.txt"))

...{
}//此處m_file便釋放了,此后m_file便指向無效資源
}
pravite RAIIFile m_file;
}
對(duì)于成員對(duì)象的RAII只能通過在析構(gòu)函數(shù)或Dispose()中手工地釋放。我還沒有想出更好的辦法來。
至此,RAII的來龍去脈已經(jīng)說清楚了,在C#里也能從中汲取到充足的養(yǎng)分。但是,這還不是RAII的全部營養(yǎng),RAII還有更多的擴(kuò)展用途。在
《Imperfect C++》一書中,Matthew
Wilson展示了RAII的一種非常重要的應(yīng)用。為了不落個(gè)鸚鵡學(xué)舌的名聲,這里我給出一個(gè)真實(shí)遇到的案例,非常簡(jiǎn)單:我寫的程序需要響應(yīng)一個(gè)Grid
控件的CellTextChange事件,執(zhí)行一些運(yùn)算。在響應(yīng)這個(gè)事件(執(zhí)行運(yùn)算)的過程中,不能再響應(yīng)同一個(gè)事件,直到處理結(jié)束。為此,我設(shè)置了一個(gè)
標(biāo)志,用來控制事件響應(yīng):
class MyForm

...{
public:

MyForm():is_cacul(false) ...{}
...

void OnCellTextChange(Cell& cell) ...{
if(is_cacul)
return;
is_cacul=true;
... //執(zhí)行計(jì)算任務(wù)
is_cacul=false;
}
private:
bool is_cacul;
};
但是,這里的代碼不是異常安全的。如果在執(zhí)行計(jì)算的過程中拋出異常,那么is_cacul標(biāo)志將永遠(yuǎn)是true。此后,即便是正常的
CellTextChange也無法得到正確地響應(yīng)。同前面遇到的資源問題一樣,傳統(tǒng)上我們不得不求助于try-catch語句。但是如果我們運(yùn)用
RAII,則可以使得代碼簡(jiǎn)化到不能簡(jiǎn)化,安全到不能再安全。我首先做了一個(gè)類:
class BoolScope

...{
public:
BoolScope(bool& val, bool newVal)

:m_val(val), m_old(val) ...{
m_val=newVal;
}

~BoolScope() ...{
m_val=m_old;
}

private:
bool& m_val;
bool m_old;
};
這個(gè)類的作用是所謂“域守衛(wèi)(scoping)”,構(gòu)造函數(shù)接受兩個(gè)參數(shù):第一個(gè)是一個(gè)bool對(duì)象的引用,在構(gòu)造函數(shù)中保存在m_val成員里;第二個(gè)
是新的值,將被賦予傳入的那個(gè)bool對(duì)象。而該對(duì)象的原有值,則保存在m_old成員中。析構(gòu)函數(shù)則將m_old的值返還給m_val,也就是那個(gè)
bool對(duì)象。有了這個(gè)類之后,便可以很優(yōu)雅地獲得異常安全:
class MyForm

...{
public:

MyForm():is_cacul(false) ...{}
...

void OnCellTextChange(Cell& cell) ...{
if(is_cacul)
return;
BoolScope bs_(is_cacul, true);
... //執(zhí)行計(jì)算任務(wù)
}
private:
bool is_cacul;
};
好啦,任務(wù)完成。在bs_創(chuàng)建的時(shí)候,is_cacul的值被替換成true,它的舊值保存在bs_對(duì)象中。當(dāng)OnCellTextChange()返回
時(shí),bs_對(duì)象會(huì)被自動(dòng)析構(gòu),析構(gòu)函數(shù)會(huì)自動(dòng)把保存起來的原值重新賦給is_cacul。一切又都回到原先的樣子。同樣,如果異常拋出,is_cacul
的值也會(huì)得到恢復(fù)。
這個(gè)BoolScope可以在將來繼續(xù)使用,分?jǐn)傁聛淼拈_發(fā)成本幾乎是0。更進(jìn)一步,可以開發(fā)一個(gè)通用的Scope模板,用于所有類型,就像《Imperfect C++》里的那樣。
下面,讓我們把戰(zhàn)場(chǎng)轉(zhuǎn)移到C#,看看C#是如何實(shí)現(xiàn)域守衛(wèi)的。考慮到C#(.net)的對(duì)象模型的特點(diǎn),我們先實(shí)現(xiàn)引用類型的域守衛(wèi),然后再來看看如何對(duì)付值類型。其原因,一會(huì)兒會(huì)看到。
我曾經(jīng)需要向一個(gè)grid中填入數(shù)據(jù),但是填入的過程中,控件不斷的刷新,造成閃爍,也影響性能,除非把控件上的AutoDraw屬性設(shè)為false。為此,我做了一個(gè)域守衛(wèi)類,在填寫操作之前關(guān)上AutoDraw,完成或異常拋出時(shí)再打開:
class DrawScope : IDisposable

...{

public DrawScope(Grid g, bool val) ...{
m_grid=g;
m_old=g->AutoDraw;
m_grid->AutoDraw=val;
}

public void Dispose() ...{
g->AutoDraw=m_old;
}
private Grid m_grid;
private bool m_old;
};
于是,我便可以如下優(yōu)雅地處理AutoDraw屬性設(shè)置問題:

static void LoadData(Grid g) ...{
using(DrawScope ds=new DrawScope(g, false))

...{
... //執(zhí)行數(shù)據(jù)裝載
}
}
現(xiàn)在,我們回過頭,來實(shí)現(xiàn)值類型的域守衛(wèi)。案例還是采用前面的CellTextChange事件。當(dāng)我試圖著手對(duì)那個(gè)is_cacul執(zhí)行域守衛(wèi)時(shí),遇到了不小的麻煩。起初,我寫下了這樣的代碼:
class BoolScope

...{
private ??? m_val; //此處用什么類型?
private bool m_old;
};
m_val應(yīng)當(dāng)是一個(gè)指向一個(gè)對(duì)象的引用,C#是沒有C++那些指針和引用的。在C#中,引用類型定義的對(duì)象實(shí)際上是一個(gè)指向?qū)ο蟮囊?;而值類型定義的
對(duì)象實(shí)際上是一個(gè)對(duì)象,或者說“棧對(duì)象”,但卻沒有一種指向值類型的引用。(關(guān)于這種對(duì)象模型的優(yōu)劣,后面的“題外話”小節(jié)有一些探討)。我嘗試著采用兩
種辦法,一種不成功,而另一種成功了。
C#(.net)有一種box機(jī)制,可以將一個(gè)值對(duì)象打包,放到堆中創(chuàng)建。這樣,或許可以把一個(gè)值對(duì)象編程引用對(duì)象,構(gòu)成C#可以引用的東西:
class BoolScope : IDisposable

...{

public BoolScope(object val, bool newVal) ...{
m_val=val; //#1
m_old=(bool)val;
(bool)m_val=newVal; //#2
}

public void Dispose() ...{
(bool)m_val=m_old; //#3
}
private object m_val;
private bool m_old;
}
使用時(shí),應(yīng)當(dāng)采用如下形式:
class MyForm

...{

public MyForm() ...{
is_cacul=new bool(false); //boxing
}
...

void OnCellTextChange(Cell& cell) ...{
if(is_cacul)
return;
using(BoolScope bs=new BoolScope(is_cacul, true))

...{
... //執(zhí)行計(jì)算任務(wù)
}
}
private object is_cacul;
};
很可惜,此路不通。因?yàn)樵诖a#1的地方,并未執(zhí)行引用語義,而執(zhí)行了值語義。也就是說,沒有把val(它是個(gè)引用)的值賦給m_val(也是個(gè)引用),
而是為m_val做了個(gè)副本。以至于在代碼#2和#3處無法將newVal和m_old賦予val(也就是is_cacul)。或許C#的設(shè)計(jì)者有無數(shù)理
由說明這種設(shè)計(jì)的合理性,但是在這里,卻扼殺了一個(gè)非常有用的idom。而且,缺少對(duì)值對(duì)象的引用手段,大大限制了語言的靈活性和擴(kuò)展性。
第二種方法就非常直白了,也絕對(duì)不應(yīng)當(dāng)出問題,就是使用包裝類:
class BoolVal

...{
public BoolVal(bool v)

...{
m_val=v;
}

public bool getVal() ...{
return m_val;
}

public void setVal(bool v) ...{
m_val=v;
}
private bool m_val;
}
class BoolScope : IDisposable

...{
public IntScope(BoolVal iv, bool v)

...{
m_old = iv.getVal();
m_Val = iv;
m_Val.setVal(v);
}
public virtual void Dispose()

...{
m_Val.setVal(m_old);
}
private BoolVal m_Val;
private bool m_old;
}
這里,我做了一個(gè)包裝類BoolVal,是個(gè)引用類。然后以此為基礎(chǔ),編寫了一個(gè)BoolScope類。然后,便可以正常使用域守衛(wèi):
class MyForm

...{

public MyForm() ...{
m_val.setVal(false); //boxing
}
...

void OnCellTextChange(Cell& cell) ...{
if(is_cacul)
return;
using(BoolScope bs=new BoolScope(m_val, true))

...{
... //執(zhí)行計(jì)算任務(wù)
}
}
private BoolVal m_val;
};
好了,一切都很不錯(cuò)。盡管C#的對(duì)象模型給我們平添了不少麻煩,使得我多寫了不少代碼,但是使用域守衛(wèi)類仍然是一本萬利的事情。作為GP fans,我當(dāng)然也嘗試著在C#里做一些泛型,以免去反復(fù)開發(fā)包裝類和域守衛(wèi)類的苦惱。這些東西,就留給大家做練習(xí)吧。:)
在某些場(chǎng)合下,我們可能會(huì)對(duì)一些對(duì)象做一些操作,完事后在恢復(fù)這個(gè)對(duì)象的原始狀態(tài),這也是域守衛(wèi)類的用武之地。只是守衛(wèi)一個(gè)結(jié)構(gòu)復(fù)雜的類,不是一件輕松的
工作。最直接的做法是取出所有的成員數(shù)據(jù),在結(jié)束后再重新復(fù)制回去。這當(dāng)然是繁復(fù)的工作,而且效率不高。但是,我們將在下一篇看到,如果運(yùn)用swap手
法,結(jié)合復(fù)制構(gòu)造函數(shù),可以很方便地實(shí)現(xiàn)這種域守衛(wèi)。這我們以后再說。
域守衛(wèi)作為RAII的一個(gè)擴(kuò)展應(yīng)用,非常簡(jiǎn)單,但卻極具實(shí)用性。如果我們對(duì)“資源”這個(gè)概念加以推廣,把一些值、狀態(tài)等等內(nèi)容都納入資源的范疇,那么域守衛(wèi)類的使用是順理成章的事。
題外話:C#的對(duì)象模型
C#的設(shè)計(jì)理念是簡(jiǎn)化語言的學(xué)習(xí)和使用。但是,就前面案例中出現(xiàn)的問題而言,在特定的情況下,特別是需要靈活和擴(kuò)展的時(shí)候,C#往往表現(xiàn)的差強(qiáng)人意。C#
的對(duì)象模型實(shí)際上是以堆對(duì)象和引用語義為核心的。不過,考慮到維持堆對(duì)象的巨大開銷和性能損失,應(yīng)用在一些簡(jiǎn)單的類型上,比如int、float等等,實(shí)
在得不嘗失。為此,C#將這些簡(jiǎn)單類型直接作為值處理,當(dāng)然也允許用戶定義自己的值類型。值類型擁有值語義。而值類型的本質(zhì)是棧對(duì)象,引用類型則是堆對(duì)
象。
這樣看起來應(yīng)該是個(gè)不錯(cuò)的折中,但是實(shí)際上卻造成了不大不小的麻煩。前面的案例已經(jīng)明確地表現(xiàn)了這種對(duì)象模型引發(fā)的麻煩。由于C#拋棄值和引用的差異(為
了簡(jiǎn)化語言的學(xué)習(xí)和使用),那么對(duì)于一個(gè)引用對(duì)象,我們無法用值語義訪問它;而對(duì)于一個(gè)值對(duì)象,我們無法用引用語義訪問。對(duì)于前者,不會(huì)引發(fā)本質(zhì)性的問
題,因?yàn)槲覀兛梢允褂贸蓡T函數(shù)來實(shí)現(xiàn)值語義。但是對(duì)于后者,則是無法逾越的障礙,就像在BoolScope案例中表現(xiàn)的那樣。在這種情況下,我們不得不用
引用類包裝值類型,使得值類型喪失了原有的性能和資源優(yōu)勢(shì)。
更有甚者,C#的對(duì)象模型有時(shí)會(huì)造成語義上的沖突。由于值類型使用值語義,而引用類型使用引用語義。那么同樣是對(duì)象定義,便有可能使用不同的語義:
int i, j=10; //值類型
i=j; //值語義,兩個(gè)對(duì)象復(fù)制內(nèi)容
i=5; //i==5, j==10
StringBuilder s1, s2 = new StringBuilder("s2"); //引用類型
s1 = s2; //引用語義,s1和s2指向同一個(gè)對(duì)象
s1.Append(" is s1"); //s1==s2=="s1 is s2"
同一個(gè)形式具有不同語義,往往會(huì)造成意想不到的問題。比如,在軟件開發(fā)的最初時(shí)刻,我們認(rèn)為某個(gè)類型是值類型就足夠了,還可以獲得性能上的好處。但是,隨
著項(xiàng)目進(jìn)入后期階段,發(fā)現(xiàn)最初的設(shè)計(jì)有問題,值類型限制了該類型的某些特性(如不能擁有析構(gòu)函數(shù),不能引用等等),那么需要把它改成引用類型。于是便引發(fā)
一大堆麻煩,需要檢查所有使用該類型的代碼,然后把
賦值操作改成
復(fù)制操作。這肯定不是討人喜歡的工作。為此,在實(shí)際開發(fā)中,很少自定義值類型,以免將來自縛手腳。于是,值類型除了語言內(nèi)置類型和.net庫預(yù)定義的類型外,成了一件擺設(shè)。
相比之下,傳統(tǒng)語言,如Ada、C、C++、Pascal等,區(qū)分引用和值的做法盡管需要初學(xué)者花更多的精力理解其中的差別,但在使用中則更加妥善和安全。畢竟學(xué)習(xí)是暫時(shí)的,使用則是永遠(yuǎn)的。