RAII:在類的建構函式中分配資源,在解構函式中釋放資源
RAII介紹及例項分析:
動物都會攝取食物,吸收其中的營養,用於自身生長和活動。然而,並非食物中所有的物質都能為動物所吸收。那些無法消化的物質,通過消化道的另一頭(某些動物消化道只有一頭)排出體外。不過,一種動物無法消化的排洩物,是另一種動物(生物)的食物,後者可以從中攝取所需的營養。
一門程式語言,對於程式設計師而言,如同食物那樣,包含著所需的養分。當然也包含著無法消化的東西。不同的是,隨著程式設計師不斷成長,會逐步消化過去無法消化的那些東西。
C++可以看作一種成分複雜的食物,對於多數程式設計師而言,是無法完全消化的。正因為如此,很多程式設計師認為C++太難以消化,不應該去吃它。但是,C++的營養不可謂不豐富,就此捨棄,而不加利用,則是莫大的罪過。好在食物可以通過加工,變得易於吸收,比如說發酵。鑑於程式設計師們的消化能力的差異,也為了讓C ++的營養能夠造福他人,我就暫且扮演一回酵母菌,把C++的某些營養單獨提取出來,並加以分解,讓那些消化能力不太強的程式設計師也能享受它的美味。:)
(為了讓這些營養便於消化,我將會用C#做一些案例。選擇C#的原因很簡單,因為我熟悉。:))
RAII
RAII,好古怪的營養啊!它的全稱應該是“Resource Acquire Is Initial”。這是C++創始人Bjarne Stroustrup發明的詞彙,比較令人費解。說起來,RAII的含義倒也不算複雜。用白話說就是:在類的建構函式中分配資源,在解構函式中釋放資源。這樣,當一個物件建立的時候,建構函式會自動地被呼叫;而當這個物件被釋放的時候,解構函式也會被自動呼叫。於是乎,一個物件的生命期結束後將會不再佔用資源,資源的使用是安全可靠的。下面便是在C++中實現RAII的典型程式碼:
{
public:
file(string const& name) {
m_fileHandle=open_file(name.cstr());
}
~file() {
close_file(m_fileHandle);
}
...
private:
handle m_fileHandle;
}
file myfile("my.txt");
... //操作檔案
} //此處銷燬物件,呼叫解構函式,釋放資源
但是,在如下的程式碼中,資源不是安全的,儘管我們實現了RAII:
file pfile=new file("my.txt");
... //操作檔案
}
file pfile=new file("my.txt");
... //操作檔案
delete pfile;
}
現在,在fun3(),資源是安全的,但卻不是異常安全的。因為一旦函式中丟擲異常,那麼delete pfile;這句程式碼將沒有機會被執行。C++領域的諸位大牛們告誡我們:如果想要在沒有GC的情況下確保資源安全和異常安全,那麼請使用智慧指標:
shared_ptr<file> spfile(new file("my.txt"));
... //操作檔案
} //此處,spfile結束生命週期的時候,會釋放(delete)物件
class smart_ptr
{
public:
smart_ptr(T* p):m_ptr(p) {}
~smart_ptr() { delete m_ptr; }
...
private:
T* m_ptr;
}
對於內建了GC的語言,資源管理相對簡單。不過,事情並非總是這樣。下面的C#程式碼摘自MSDN Library的C#程式設計指南,我略微改造了一下:
{
System.IO.FileStream file = null;
System.IO.FileInfo fileInfo = new System.IO.FileInfo("C:file.txt");
file = fileInfo.OpenWrite();
file.WriteByte(0xF);
}
所以,只要涉及了記憶體以外的資源,應當儘快釋放。(當然,如果記憶體能夠儘快釋放,就更好了)。對於上述CodeWithoutCleanup()函式,應當在最後呼叫file物件上的Close()函式,以便釋放檔案:
{
System.IO.FileStream file = null;
System.IO.FileInfo fileInfo = new System.IO.FileInfo("C:file.txt");
file = fileInfo.OpenWrite();
file.WriteByte(0xF);
file.Close();
}
{
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();
}
}
}
一個有效的RAII應當包含兩個部分:構造/解構函式的資源獲取/釋放和確定性的解構函式呼叫。前者在C#中不成問題,C#有建構函式和解構函式。不過, C#的建構函式和解構函式是不能用於RAII的,原因一會兒會看到。正確的做法是讓一個類實現IDisposable介面,在IDisposable:: Dispose()函式中釋放資源:
{
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;
}
{
using(RAIIFile file=new RAIIFile("C:file.txt"))
{
... //操作檔案
} //檔案釋放
}
但是,還有一個問題是using無法解決的,就是如何維持類的成員函式的RAII。我們希望一個類的成員物件在該類例項建立的時候獲取資源,而在其銷燬的時候釋放資源:
{
public:
X():m_file("c:file.txt") {}
private:
File m_file; //在X的例項析構時呼叫File::~File(),釋放資源。
}
{
public X() {
using(m_file=new RAIIFile("C:file.txt"))
{
}//此處m_file便釋放了,此後m_file便指向無效資源
}
pravite RAIIFile m_file;
}
至此,RAII的來龍去脈已經說清楚了,在C#裡也能從中汲取到充足的養分。但是,這還不是RAII的全部營養,RAII還有更多的擴充套件用途。在《Imperfect C++》一書中,Matthew Wilson展示了RAII的一種非常重要的應用。為了不落個鸚鵡學舌的名聲,這裡我給出一個真實遇到的案例,非常簡單:我寫的程式需要響應一個Grid 控制元件的CellTextChange事件,執行一些運算。在響應這個事件(執行運算)的過程中,不能再響應同一個事件,直到處理結束。為此,我設定了一個標誌,用來控制事件響應:
{
public:
MyForm():is_cacul(false) {}
...
void OnCellTextChange(Cell& cell) {
if(is_cacul)
return;
is_cacul=true;
... //執行計算任務
is_cacul=false;
}
private:
bool is_cacul;
};
{
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;
};
{
public:
MyForm():is_cacul(false) {}
...
void OnCellTextChange(Cell& cell) {
if(is_cacul)
return;
BoolScope bs_(is_cacul, true);
... //執行計算任務
}
private:
bool is_cacul;
};
這個BoolScope可以在將來繼續使用,分攤下來的開發成本幾乎是0。更進一步,可以開發一個通用的Scope模板,用於所有型別,就像《Imperfect C++》裡的那樣。
下面,讓我們把戰場轉移到C#,看看C#是如何實現域守衛的。考慮到C#(.net)的物件模型的特點,我們先實現引用型別的域守衛,然後再來看看如何對付值型別。其原因,一會兒會看到。
我曾經需要向一個grid中填入資料,但是填入的過程中,控制元件不斷的重新整理,造成閃爍,也影響效能,除非把控制元件上的AutoDraw屬性設為false。為此,我做了一個域守衛類,在填寫操作之前關上AutoDraw,完成或異常丟擲時再開啟:
{
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;
};
using(DrawScope ds=new DrawScope(g, false))
{
... //執行資料裝載
}
}
{
private ??? m_val; //此處用什麼型別?
private bool m_old;
};
C#(.net)有一種box機制,可以將一個值物件打包,放到堆中建立。這樣,或許可以把一個值物件程式設計引用物件,構成C#可以引用的東西:
{
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;
}
{
public MyForm() {
is_cacul=new bool(false); //boxing
}
...
void OnCellTextChange(Cell& cell) {
if(is_cacul)
return;
using(BoolScope bs=new BoolScope(is_cacul, true))
{
... //執行計算任務
}
}
private object is_cacul;
};
第二種方法就非常直白了,也絕對不應當出問題,就是使用包裝類:
{
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;
}
{
public MyForm() {
m_val.setVal(false); //boxing
}
...
void OnCellTextChange(Cell& cell) {
if(is_cacul)
return;
using(BoolScope bs=new BoolScope(m_val, true))
{
... //執行計算任務
}
}
private BoolVal m_val;
};
在某些場合下,我們可能會對一些物件做一些操作,完事後在恢復這個物件的原始狀態,這也是域守衛類的用武之地。只是守衛一個結構複雜的類,不是一件輕鬆的工作。最直接的做法是取出所有的成員資料,在結束後再重新複製回去。這當然是繁複的工作,而且效率不高。但是,我們將在下一篇看到,如果運用swap手法,結合複製建構函式,可以很方便地實現這種域守衛。這我們以後再說。
域守衛作為RAII的一個擴充套件應用,非常簡單,但卻極具實用性。如果我們對“資源”這個概念加以推廣,把一些值、狀態等等內容都納入資源的範疇,那麼域守衛類的使用是順理成章的事。
題外話:C#的物件模型
C#的設計理念是簡化語言的學習和使用。但是,就前面案例中出現的問題而言,在特定的情況下,特別是需要靈活和擴充套件的時候,C#往往表現的差強人意。C# 的物件模型實際上是以堆物件和引用語義為核心的。不過,考慮到維持堆物件的巨大開銷和效能損失,應用在一些簡單的型別上,比如int、float等等,實在得不嘗失。為此,C#將這些簡單型別直接作為值處理,當然也允許使用者定義自己的值型別。值型別擁有值語義。而值型別的本質是棧物件,引用型別則是堆物件。這樣看起來應該是個不錯的折中,但是實際上卻造成了不大不小的麻煩。前面的案例已經明確地表現了這種物件模型引發的麻煩。由於C#拋棄值和引用的差異(為了簡化語言的學習和使用),那麼對於一個引用物件,我們無法用值語義訪問它;而對於一個值物件,我們無法用引用語義訪問。對於前者,不會引發本質性的問題,因為我們可以使用成員函式來實現值語義。但是對於後者,則是無法逾越的障礙,就像在BoolScope案例中表現的那樣。在這種情況下,我們不得不用引用類包裝值型別,使得值型別喪失了原有的效能和資源優勢。
更有甚者,C#的物件模型有時會造成語義上的衝突。由於值型別使用值語義,而引用型別使用引用語義。那麼同樣是物件定義,便有可能使用不同的語義:
i=j; //值語義,兩個物件複製內容
i=5; //i==5, j==10
StringBuilder s1, s2 = new StringBuilder("s2"); //引用型別
s1 = s2; //引用語義,s1和s2指向同一個物件
s1.Append(" is s1"); //s1==s2=="s1 is s2"
同一個形式具有不同語義,往往會造成意想不到的問題。比如,在軟體開發的最初時刻,我們認為某個型別是值型別就足夠了,還可以獲得效能上的好處。但是,隨著專案進入後期階段,發現最初的設計有問題,值型別限制了該型別的某些特性(如不能擁有解構函式,不能引用等等),那麼需要把它改成引用型別。於是便引發一大堆麻煩,需要檢查所有使用該型別的程式碼,然後把賦值操作改成複製操作。這肯定不是討人喜歡的工作。為此,在實際開發中,很少自定義值型別,以免將來自縛手腳。於是,值型別除了語言內建型別和.net庫預定義的型別外,成了一件擺設。
相比之下,傳統語言,如Ada、C、C++、Pascal等,區分引用和值的做法儘管需要初學者花更多的精力理解其中的差別,但在使用中則更加妥善和安全。畢竟學習是暫時的,使用則是永遠的。
相關文章
- 類的建構函式和解構函式函式
- C++中建構函式,拷貝建構函式和賦值函式的詳解C++函式賦值
- 建構函式與解構函式函式
- 預設建構函式、引數化建構函式、複製建構函式、解構函式函式
- C++ 類建構函式和解構函式C++函式
- 虛解構函式(√)、純虛解構函式(√)、虛建構函式(X)函式
- dart系列之:dart類中的建構函式Dart函式
- 繼承中的建構函式繼承函式
- C#中的建構函式C#函式
- 關於建構函式與解構函式的分享函式
- PHP筆記:建構函式與解構函式PHP筆記函式
- 拷貝建構函式中的陷阱函式
- android中Fragment的建構函式AndroidFragment函式
- 資源管理器中編寫分類器函式函式
- C#中解構函式,Close函式,Dispose函式的區別C#函式
- 原則9:使用解構函式防止資源洩露函式
- C++語言之結構體、類、建構函式、拷貝建構函式C++結構體函式
- C#中的解構函式C#函式
- JS 建構函式與類JS函式
- 執行緒join為什麼在解構函式中執行緒函式
- 建構函式顯式返回 this 在 new 運算及 call 方法中的比較函式
- 建構函式詳解函式
- C++ 建構函式和解構函式C++函式
- C++建構函式解構函式的執行過程C++函式
- ## 建構函式函式
- 建構函式函式
- 關於scala中的主建構函式函式
- 建構函式中丟擲的異常函式
- 函式中的指標分配的記憶體怎麼釋放函式指標記憶體
- [cpp]C++中的解構函式C++函式
- [譯] 建構函式已死,建構函式萬歲!函式
- 19-父類的建構函式函式
- flutter-dart 類的建構函式FlutterDart函式
- C/C++——建構函式、複製建構函式和解構函式的執行時刻C++函式
- 在 C++ 中子類繼承和呼叫父類的建構函式方法C++繼承函式
- 在C++中子類繼承和呼叫父類的建構函式方法C++繼承函式
- 在 TypeScript 的類元件的建構函式中是否總是需要定義 `props` 和 `state` ?TypeScript元件函式
- Java建構函式詳解Java函式