首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> C# >> 關於C#

C# 建構函式與解構函式深度解析:記憶體管理、繼承鏈與最佳實務

     編輯:關於C#

前言

建構函式負責物件誕生時的初始化,解構函式 (Finalizer) 則是非受控資源釋放的最後防線。本文系統說明各類建構函式語意、繼承鏈執行順序、GC 與 Finalize 機制、IDisposable 模式、以及常見陷阱與最佳實務。

一、建構函式完整分類

1.1 執行個體建構函式


public class Point
{
    public int X { get; set; }
    public int Y { get; set; }

    // 無參數建構函式
    public Point() : this(0, 0) { } // 鏈結呼叫

    // 參數化建構函式
    public Point(int x, int y)
    {
        X = x; Y = y;
    }

    // 運算式主體建構函式 (C# 7+)
    public Point(double polarR, double polarTheta) => 
        (X, Y) = ((int)(polarR * Math.Cos(polarTheta)), (int)(polarR * Math.Sin(polarTheta)));
}

1.2 靜態建構函式

  • 關鍵字 static,無參數、無存取修飾詞
  • 在類別第一次被存取前執行 (靜態欄位初始化、靜態方法呼叫、建立例項)
  • 執行緒安全,僅執行一次
  • 用途:初始化靜態資源、註冊相依性、載入原生 DLL

public class Database
{
    private static readonly string _connectionString;
    private static readonly SemaphoreSlim _semaphore = new(1);

    static Database()
    {
        _connectionString = LoadConfig(); // 複雜初始化邏輯
        Console.WriteLine("靜態建構函式執行");
    }
}

1.3 主要建構函式 (C# 12+ / Record)


// Record 自動產生主要建構函式
public record Person(string Name, int Age);

// 類別主要建構函式 (C# 12)
public class Employee(string name, int age)
{
    public string Name { get; } = name;
    public int Age { get; } = age;
}

二、繼承鏈建構函式執行順序

2.1 核心規則

  1. 衍生類別建構函式第一行必須呼叫 base(...) 或 this(...)
  2. 若省略,編譯器自動插入 base() (呼叫基底類別無參建構函式)
  3. 基底類別建構函式完全執行完畢後,才執行衍生類別建構函式本體
  4. 欄位初始化運算式在基底建構函式呼叫之前執行

2.2 執行順序圖解


class Base
{
    public Base() { Console.WriteLine("Base()"); }
    public Base(int x) { Console.WriteLine($"Base({x})"); }
}

class Derived : Base
{
    private int _y = InitY(); // 1. 衍生欄位初始化
    private static int InitY() { Console.WriteLine("Derived field init"); return 10; }

    public Derived() : base(5) // 2. 顯式呼叫 base(5)
    {
        Console.WriteLine("Derived()"); // 4. 衍生建構函式本體
    }
}

// 輸出順序:
// Derived field init
// Base(5)
// Derived()

2.3 危險:基底建構函式呼叫虛擬方法


class Base
{
    public Base() { Initialize(); } // 危險!
    protected virtual void Initialize() { Console.WriteLine("Base Initialize"); }
}

class Derived : Base
{
    private readonly List _items = new(); // 欄位初始化
    protected override void Initialize() 
    {
        Console.WriteLine("Derived Initialize");
        _items.Add("test"); // NullReferenceException!_items 尚未初始化
    }
}

鐵律:建構函式中絕不呼叫虛擬方法 (直接或間接)。

三、解構函式與記憶體管理

3.1 解構函式語法與語意


public class ResourceHolder : IDisposable
{
    private IntPtr _unmanagedHandle;
    private bool _disposed;

    // 解構函式 (Finalizer)
    ~ResourceHolder()
    {
        Dispose(false); // 僅釋放非受控資源
    }

    // 公用 Dispose
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this); // 告訴 GC 不需呼叫 Finalize
    }

    // 核心釋放邏輯
    protected virtual void Dispose(bool disposing)
    {
        if (_disposed) return;

        if (disposing)
        {
            // 釋放受控資源 (Managed)
            // managedResource?.Dispose();
        }

        // 釋放非受控資源 (Unmanaged)
        if (_unmanagedHandle != IntPtr.Zero)
        {
            NativeMethods.CloseHandle(_unmanagedHandle);
            _unmanagedHandle = IntPtr.Zero;
        }

        _disposed = true;
    }
}

3.2 GC 與 Finalize 機制

  • 物件佔用記憶體在 Managed Heap
  • GC 掃描到不可觸及物件 → 若有 Finalizer → 放入 Finalization Queue
  • 專用 Finalizer 執行緒依序呼叫 Finalize (~ClassName)
  • 下一次 GC 才真正回收記憶體
  • 成本:有 Finalizer 的物件至少存活兩輪 GC (Gen 0 → Gen 1 → Gen 2)

3.3 為什麼不建議隨意寫解構函式?

  1. 效能損耗:Finalizer 執行緒單一、順序執行、阻塞會拖垮整個 GC
  2. 非決定性:無法預測何時執行,不適合釋放稀缺資源 (檔案鎖、資料庫連線)
  3. 例外處理:Finalizer 中拋出未捕獲例外會終止進程 (.NET Framework) 或被忽略 (.NET Core+)
  4. 記憶體洩漏風險:忘記 GC.SuppressFinalize 導致重複釋放

四、IDisposable 模式完整實作 (標準模式)


public class DisposableBase : IDisposable
{
    private bool _disposed;

    // 公開 Dispose
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    // 核心虛擬方法,衍生類別覆寫
    protected virtual void Dispose(bool disposing)
    {
        if (_disposed) return;

        if (disposing)
        {
            // 釋放受控資源
            ReleaseManagedResources();
        }

        // 釋放非受控資源
        ReleaseUnmanagedResources();

        _disposed = true;
    }

    // 解構函式:僅釋放非受控資源
    ~DisposableBase() => Dispose(false);

    // 衍生類別實作
    protected virtual void ReleaseManagedResources() { }
    protected virtual void ReleaseUnmanagedResources() { }

    // 防護:公開方法檢查是否已釋放
    protected void ThrowIfDisposed()
    {
        if (_disposed) throw new ObjectDisposedException(GetType().Name);
    }
}

// 使用範例
public class FileProcessor : DisposableBase
{
    private FileStream _stream;

    public void Open(string path) 
    { 
        ThrowIfDisposed();
        _stream = File.OpenRead(path); 
    }

    protected override void ReleaseManagedResources()
    {
        _stream?.Dispose();
        _stream = null;
    }
}

五、現代替代方案:SafeHandle

.NET 2.0+ 提供 SafeHandle 封裝非受控控制碼,自動處理 Finalize、參考計數、關鍵區段:


public class SafeFileHandle : SafeHandleZeroOrMinusOneIsInvalid
{
    public SafeFileHandle(IntPtr handle, bool ownsHandle) : base(ownsHandle) { SetHandle(handle); }
    protected override bool ReleaseHandle() => NativeMethods.CloseHandle(handle);
}

// 使用
using var handle = new SafeFileHandle(NativeMethods.CreateFile(...), true);
// 區塊結束自動釋放,無需寫 Finalizer

六、常見陷阱總結表

陷阱後果解決
建構函式呼叫虛擬方法存取未初始化欄位建構函式只呼叫非虛擬/私有方法
衍生類別未呼叫 base()編譯錯誤或基底未初始化顯式呼叫 base(參數)
解構函式釋放受控資源受控物件可能已被 GC 回收Dispose(false) 只釋放非受控
忘記 GC.SuppressFinalize重複釋放、效能損耗Dispose() 最後呼叫
非受控資源未實作 IDisposable資源洩漏 (檔案鎖、連線)實作標準 Dispose 模式
Finalizer 中拋出例外進程終止 / 執行緒中止Finalizer 內全部 try-catch

七、總結

  • 建構函式:確保物件誕生即進入有效狀態,善用鏈結呼叫、欄位初始化、主要建構函式
  • 繼承順序:基底 → 衍生欄位初始化 → 基底建構函式本體 → 衍生建構函式本體
  • 解構函式:唯一用途是釋放非受控資源,配合 IDisposable 實作決定性釋放
  • 最佳實務:優先使用 using / SafeHandle / 容器管理生命週期,極少情況需手寫 Finalizer
  • 核心原則:建構函式不呼叫虛擬方法、Dispose 可重複呼叫、Finalizer 只清理非受控資源
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved