跳轉到內容

C# 程式設計/物件生命週期

來自華夏公益教科書,開放的書,開放的世界

所有計算機程式都會佔用記憶體,無論是記憶體中的變數,開啟檔案還是連線到資料庫。問題是如何讓執行時環境在記憶體不再使用時回收記憶體?這個問題有三個答案

  • 如果您使用的是 託管 資源,垃圾收集器會自動釋放它。
  • 如果您使用的是 非託管 資源,則必須使用 IDisposable 介面來協助清理。
  • 如果您直接呼叫垃圾收集器,透過使用 System.GC.Collect() 方法,它將被強制立即清理資源。

在討論託管和非託管資源之前,瞭解垃圾收集器究竟做了什麼將很有趣。

垃圾收集器

[編輯 | 編輯原始碼]

垃圾收集器是您的程式中執行的後臺程序。它始終存在於所有 .NET 應用程式中。它的作用是查詢程式不再使用的物件(即引用型別)。如果物件被分配為 null,或者物件超出範圍,垃圾收集器將標記該物件將在將來的某個時間點被清理,而不會立即釋放其資源!

為什麼?垃圾收集器很難跟上您所做的每一次取消分配,尤其是在程式執行的速度下,因此它只會在資源變得有限時執行。因此,垃圾收集器有三個“代”。

  • 第 0 代 - 最近建立的物件
  • 第 1 代 - 中年物件
  • 第 2 代 - 長期物件。

所有引用型別都將存在於這三個代中的一個。它們首先將被分配到第 0 代,然後根據它們的壽命移動到第 1 代和第 2 代。垃圾收集器的工作原理是隻刪除必要的內容,因此只會掃描第 0 代以找到快速解決方案。這是因為大多數(如果不是全部)區域性變數都放置在此區域。

有關更深入的資訊,請訪問 MSDN 文章 以獲得更好的解釋。

現在您已經瞭解了垃圾收集器,讓我們討論一下它正在管理的資源。

託管資源

[編輯 | 編輯原始碼]

託管資源是在 .NET 框架內完全執行的物件。所有記憶體都會自動為您回收,所有資源都會關閉,並且在大多數情況下,您有保證在應用程式關閉後或垃圾收集器執行時所有記憶體都會被釋放。

您無需對它們進行任何操作來關閉連線或任何其他操作,它是一個自清理物件。

非託管資源

[編輯 | 編輯原始碼]

在某些情況下,.NET 框架世界不會釋放資源。這可能是因為物件引用 .NET 框架外部的資源,例如作業系統,或者在內部引用另一個非託管元件,或者訪問使用 COM、COM+ 或 DCOM 的元件。

無論是什麼原因,如果您使用的是在類級別實現 IDisposable 介面的物件,那麼您也需要實現 IDisposable 介面。

public interface IDisposable
{
     void Dispose();
}

此介面公開一個名為 Dispose() 的方法。這本身 不會 幫助清理資源,因為它只是一個介面,因此開發人員必須正確使用它才能確保資源被釋放。兩個步驟是

  1. 在完成使用任何實現 IDisposable 的物件後,始終呼叫 Dispose()。(這可以使用 using 關鍵字更輕鬆地實現。)
  2. 使用 finalizer 方法呼叫 Dispose(),這樣如果有人沒有關閉您的資源,您的程式碼將為您完成。

Dispose 模式

[編輯 | 編輯原始碼]

通常,您想要清理的內容取決於您的物件是否正在被終結。例如,您不希望在 finalizer 中清理託管資源,因為託管資源可能已經被垃圾收集器回收了。dispose 模式 可以幫助您在這種情況下正確實現資源管理。

public class MyResource : IDisposable
{
    private IntPtr _someUnmanagedResource;
    private List<long> _someManagedResource = new List<long>();
    
    public MyResource()
    {
        _someUnmanagedResource = AllocateSomeMemory();
        
        for (long i = 0; i < 10000000; i++)
            _someManagedResource.Add(i);
        ...
    }
    
    // The finalizer will call the internal dispose method, telling it not to free managed resources.
    ~MyResource()
    {
        this.Dispose(false);
    }
    
    // The internal dispose method.
    private void Dispose(bool disposing)
    {
        if (disposing)
        {
            // Clean up managed resources
            _someManagedResource.Clear();
        }
        
        // Clean up unmanaged resources
        FreeSomeMemory(_someUnmanagedResource);
    }
    
    // The public dispose method will call the internal dispose method, telling it to free managed resources.
    public void Dispose()
    {
        this.Dispose(true);
        // Tell the garbage collector to not call the finalizer because we have already freed resources.
        GC.SuppressFinalize(this);
    }
}

如果您來自 Visual Basic Classic,您將看到類似這樣的程式碼

Public Function Read(ByRef FileName) As String
  
    Dim oFSO As FileSystemObject
    Set oFSO = New FileSystemObject
  
    Dim oFile As TextStream
    Set oFile = oFSO.OpenTextFile(FileName, ForReading, False)
    Read = oFile.ReadLine
    
End Function

請注意,oFSOoFile 都沒有明確處置。在 Visual Basic Classic 中,這是沒有必要的,因為這兩個物件都是區域性宣告的。這意味著引用計數在函式結束時變為零,這會導致對這兩個物件的 Terminate 事件處理程式的呼叫。這些事件處理程式會關閉檔案並釋放相關的資源。

在 C# 中,這種情況不會發生,因為物件沒有引用計數。終結器將不會在垃圾收集器決定處置物件之前被呼叫。如果程式使用的記憶體很少,這可能需要很長時間。

這會導致問題,因為檔案保持開啟狀態,這可能會阻止其他程序訪問它。

在許多語言中,解決方案是顯式關閉檔案並處置物件,許多 C# 程式設計師也正是這樣做的。但是,還有更好的方法:使用 using 語句

public read(string fileName)
{
    using (TextReader textReader = new StreamReader(filename))
    {
        return textReader.ReadLine();
    }
}

在幕後,編譯器將 using 語句轉換為 try ... finally 並生成此中間語言 (IL) 程式碼

.method public hidebysig static string  Read(string FileName) cil managed
{
    // Code size       39 (0x27)
    .maxstack  5
    .locals init (class [mscorlib]System.IO.TextReader V_0, string V_1)
    IL_0000:  ldarg.0
    IL_0001:  newobj     instance void [mscorlib]System.IO.StreamReader::.ctor(string)
    IL_0006:  stloc.0
    .try
    {
        IL_0007:  ldloc.0
        IL_0008:  callvirt   instance string [mscorlib]System.IO.TextReader::ReadLine()
        IL_000d:  stloc.1
        IL_000e:  leave      IL_0025
        IL_0013:  leave      IL_0025
    }  // end .try
    finally
    {
        IL_0018:  ldloc.0
        IL_0019:  brfalse    IL_0024
        IL_001e:  ldloc.0
        IL_001f:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
        IL_0024:  endfinally
    }  // end handler
    IL_0025:  ldloc.1
    IL_0026:  ret
} // end of method Using::Read

請注意,Read 函式的主體已分為三個部分:初始化、try 和 finally。finally 塊包含從未在原始 C# 原始碼中顯式指定的程式碼,即對 Streamreader 例項的 解構函式 的呼叫。

請參閱 理解 C# 中的 'using' 語句 由 TiNgZ aBrAhAm

請參閱以下部分以瞭解此技術的更多應用。

資源獲取即初始化

[編輯 | 編輯原始碼]

介紹中 using 語句的應用是稱為 資源獲取即初始化 (RAII) 的慣用法的一個示例。

RAII 是一種在 Visual Basic Classic 和 C++ 等具有確定性終結的語言中自然而然的技術,但在垃圾回收語言(如 C# 和 VB.NET)中編寫的程式中通常需要額外的工作才能包含進來。using 語句使它變得同樣容易。當然,您可以顯式地編寫 try..finally 程式碼,在某些情況下仍然需要這樣做。有關 RAII 技術的深入討論,請參見 HackCraft: The RAII Programming Idiom。維基百科也對此主題進行了簡要說明:Resource Acquisition Is Initialization.

正在進行的工作:新增 C# 版本,展示帶有和不帶有 using 的不正確和正確方法。新增關於 RAII、記憶和快取的說明(參見 OOP 華夏公益教科書)。

華夏公益教科書