CLR 有一個 垃圾收集GC 機制,可以管理內存分配和回收等工作,在絕大多數情況下,程序員只需要new 一個對象,而將銷毀這一對象的工作
完全交給CLR代勞。
但是,我們所編寫的類中使用了非托管的資源,比如文件句柄,用於線程同步的Mutex對象,或者是數據庫連接,這些資源應該遵循“即需即建即銷毀”的原則,
這就是說:需要的時候才創建這些對象,用完之后就馬上銷毀。
析構函數(destructor) 與構造函數相反,當對象脫離其作用域時(例如對象所在的函數已調用完畢),系統自動執行析構函數。析構函數往往用來做“清理善后” 的工作(例如在建立對象時用new開辟了一片內存空間,應在退出前在析構函數中用delete釋放)。
以C++語言為例,析構函數名也應與類名相同,只是在函數名前面加一個波浪符~,例如~stud( ),以區別於構造函數。它不能帶任何參數,也沒有返回值(包括void類型)。只能有一個析構函數,不能重載。如果用戶沒有編寫析構函數,編譯系統會自動生成一個缺省的析構函數,它也不進行任何操作。所以許多簡單的類中沒有用顯式的析構函數。
解構器
我們知道,‘解構器’被用來清除類的事例。當我們在C#中使用解構器是,我們必須記住以下幾點:
一個類只能有一個解構器。
解構器不能被繼承或重載。
解構器不能被調用。他們是自動被(編譯器)調用的。
解構器不能帶修飾或參數。
下面是類MyClass解構器的一個聲明:
~ Class()
{
// Cleaning up code goes here
}
程序員不能控制解構器何時將被執行因為這是由垃圾收集器決定的。垃圾收集器檢查不在被應用程序使用的對象。它認為這些條件是符合清楚的並且收回它們的內存。解構器也在程序退出時被調用。當解構器執行時其背后所發生的那一幕是解構器隱式調用對象基類的Object.Finalize方法。因此上述解構器代碼被隱含轉化成:
protected override void Finalize()
{
try
{
// Cleaning up .
}
finally
{
base.Finalize();
}
}
現在,讓我們看一個解構器怎樣被調用的例子。我們有三個類A,B和C 。B派生自A,C派生自B。每個類有它們自己的構造器和解構。在類App的main函數中,我們創建C的對象。
using System;
class A
{
public A()
{
Console.WriteLine("Creating A");
}
~A()
{
Console.WriteLine("Destroying A");
}
}
class B:A
{
public B()
{
Console.WriteLine("Creating B");
}
~B()
{
Console.WriteLine("Destroying B");
}
}
class C:B
{
public C()
{
Console.WriteLine("Creating C");
}
~C()
{
Console.WriteLine("Destroying C");
}
}
class App
{
public static void Main()
{
C c=new C();
Console.WriteLine("Object Created ");
Console.WriteLine("Press enter to Destroy it");
Console.ReadLine();
c=null;
//GC.Collect();
Console.Read();
}
}
正如我們預料的,基類的構造器將會被執行並且程序會等待用戶按‘enter’。當這個發生,我們把類C的對象置為null.但解構器沒有被執行..!!??正像我們所說的,程序員無法控制解構器何時被執行因為這是由垃圾搜集器決定的。但程序退出時解構器被調用了。你能通過重定向程序的o/p到文本文件來檢查這個。我將它輸出在這里。注意到基類的解構器被調用了,因為在背后base.Finalize()被調用了。
Creating A
Creating B
Creating C
Object Created
Press enter to Destroy it
Destroying C
Destroying B
Destroying A
所以,如果一旦你使用完對象你就想調用解構器,你該怎么做?有兩個方法:
調用垃圾搜集器來清理。
實現IDisposable的Dispose方法。
調用垃圾搜集器
你能通過調用GC.Collect方法強制垃圾搜集器來清理內存,但在大多數情況下,這應該避免因為它會導致性能問題。在上面的程序中,在GC.Collect()處移除注釋。編譯並運行它。現在,你能看到解構器在控制台中被執行了。
以過上面的分析,我們要明確,當CLR的垃圾收集線程要回收一個定義了析構函數的對象時,它會自動調用其Finalize方法。
析構函數存在的主要目的是釋放非托管的資源。但是,對象的析構函數(即Finalize方法)被調用的時機是不可控的,因為它的調用由CLR的垃圾收集機制
負責,出於性能考慮,CLR的垃圾收集線程只是在“它認為是合適的”時機才運行,這樣一來就有可能出現對象所占有的非托管資源遲遲不能得到釋放的情況。
最好有一種方法能夠讓程序員主動地以完全可控的方式去釋放這些非托管的資源,為此.NET提供了一個IDispose接口。
實現IDisposable接口
IDisposable 接口包括僅有的一個公共方法,其聲明為void Dispose()。我們能實現這個方法來關閉或釋放非托管資源如實現了這個接口的類事例所控制的文件,流,和句柄等。這個方法被用做所有任務聯合對象的資源釋放。當實現了這個方法,對象必須尋求確保所有擁有的資源被繼承結構中關聯的資源也釋放(不能把握,翻不出來)。
class MyClass:IDisposable
{
public void Dispose()
{
//implementation
}
}
當我們實現了IDisposable接口時,我們需要規則來確保Dispose被適當地調用。
這里有一個問題,CLR不認為IDisposable接口與其他接口有什么不同,因CLR的垃圾收集線程不會主動地詢問對象是否實現了IDisposable接口
並自動調用它的Dispose方法。另外,很多可能程序員在開發時會忘記調用對象的Dispose方法,為了避免資源泄露,我們讓對象的析構函數也調用Dispose方法,
這就雙保險了。如以下代碼所示:
聯合使用解構器和IDisposable接口
Public class MyClass:IDisposable
{
private bool IsDisposed=false;
public void Dispose()
{
Dispose(true);
GC.SupressFinalize(this);
}
protected void Dispose(bool Diposing)
{
if(!IsDisposed)
{
if(Disposing)
{
//Clean Up managed resources
}
//Clean up unmanaged resources
}
IsDisposed=true;
}
~MyClass()
{
Dispose(false);
}
}
這個代碼框架看上去不太好理解,首先,給類添加一個Dispose(bool),此方法接收一個bool類型的參數,當參數值為true 時,可以編寫一些必要的代碼來清理托管的資源,比如
調用本對象所引用的其他托管對象的Dispose方法。不管參數值如何,此Dispose(bool)方法都必須完成清理非托管資源的任務。
當用戶在應用 程序中顯式調用IDisposable接口的Dispose方法時,此方法以true作為實參調用上面定義的虛方法Dispose(bool),並通知CLR的垃圾收集線程,不要再調用此對象的Finalize方法。
如果用戶沒有顯式調用IDisposable接口的Dispose方法,則由CLR的垃圾收集線程負責調用對象的Finalize方法完成資源清理工作,注意這時傳入的參數是false,國為finalize方法只負責釋放非托管的資源。
~MyClass()
{
Dispose(false);
}
在這里重載了Dispose(bool)來做清理工作,並且所有的清理代碼都僅寫在這個方法中。這個方法被解構器和IDisposable.Dispose()兩着調用。我們應該注意Dispose(bool)沒有在任何地方被調用除了在IDisposable.Dispose()和解構器中。
當一個客戶調用IDisposable.Dispose()時,客戶特意地想要清理托管的和非托管資源,並且因此完成清理工作。有一件你必須注意的事情是我們在清理資源之后立即調用了GC.SupressFinalize(this)。這個方法通知垃圾搜集器不需要調用解構器,因為我們已經做了清理。
注意上面的例子,解構器使用參數false調用Dispose。這里,我們確信垃圾搜集器搜集了托管資源。我們僅僅做非托管資源的清理。
結論
盡管如此我們花費一些時間實現IDisposable接口,如果客戶不能合適地調用它們會怎樣?為此C#有一個酷的解決方案。‘using’代碼塊。它看起來像這樣:
using (MyClass objCls =new MyClass())
{
}
當控制從using塊通過成功運行到結束或者拋出異常退出時,MyClass的IDispose.Dispose()將會被執行。記住你例示的對象必須實現System.IDisposable接口。using語句定義了哪個對象將被清除的一個范圍。
注:
構造函數與析構函數的區別:
構造函數和析構函數是在類體中說明的兩種特殊的成員函數。
構造函數的功能是在創建對象時,使用給定的值來將對象初始化。
析構函數的功能是用來釋放一個對象的。在對象刪除前,用它來做一些清理工作,它與構造函數的功能正好相反。