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

C# 2.0 泛型實現單例模式重用:SingletonProvider 通用解決方案

     編輯:關於C#

前言

單例模式 是最廣為人知的設計模式,但傳統實作有個痛點:**每個單例類別都要重寫相同的樣板程式碼** (私有建構子、靜態實例欄位、公開靜態 Instance 屬性、執行緒安全鎖定)。C# 2.0 引入泛型後,我們可以將這些共同邏輯抽離為 **`SingletonProvider`** 通用類別,實現「寫一次,處處復用」。

傳統單例實作痛點


// 每個單例類別都要寫這麼多重複代碼
public sealed class Logger
{
    private static readonly Logger _instance = new Logger();
    private static readonly object _lock = new();
    private Logger() { }
    
    public static Logger Instance
    {
        get
        {
            // 雙重檢查鎖定
            if (_instance == null)
            {
                lock (_lock)
                {
                    if (_instance == null)
                        return _instance; // 實際上靜態初始化已保證執行緒安全
                }
            }
            return _instance;
        }
    }
    
    public void Log(string msg) => Console.WriteLine($"[LOG] {msg}");
}

泛型 SingletonProvider 實作

核心實作 (執行緒安全、延遲初始化)


using System;

/// 
/// 通用單例提供者:為任意類別提供單例存取
/// 
/// 目標類別,必須有公用無參數建構子
public class SingletonProvider where T : new()
{
    // 私有建構子防止外部實例化
    private SingletonProvider() { }

    // 延遲初始化靜態實例 (C# 保證執行緒安全)
    private static readonly Lazy _instance = new Lazy(() => new T());

    /// 
    /// 取得單例實例 (執行緒安全、延遲初始化)
    /// 
    public static T Instance => _instance.Value;
}

進階版:支援建構子參數與自定初始化邏輯


/// 
/// 進階單例提供者:支援參數化建構與初始化委派
/// 
public class SingletonProvider where T : class
{
    private SingletonProvider() { }

    // 使用 Lazy 確保執行緒安全與延遲初始化
    private static readonly Lazy _instance = new Lazy(CreateInstance, true);

    // 建立實例的委派 (可在首次存取前設定)
    private static Func _factory = () => Activator.CreateInstance();

    /// 
    /// 設定自定建立邏輯 (必須在首次存取 Instance 前呼叫)
    /// 
    public static void Configure(Func factory)
    {
        if (_instance.IsValueCreated)
            throw new InvalidOperationException("單例已建立,無法重新配置");
        _factory = factory ?? throw new ArgumentNullException(nameof(factory));
    }

    /// 
    /// 重設單例 (僅供測試使用)
    /// 
    internal static void ResetForTesting()
    {
        // 注意:Lazy 無法重設,測試時建議使用介面抽象而非直接依賴 SingletonProvider
    }

    private static T CreateInstance() => _factory();

    public static T Instance => _instance.Value;
}

// 使用範例:參數化建構
public class DatabaseConnection
{
    public string ConnectionString { get; }
    public int TimeoutSeconds { get; }

    public DatabaseConnection(string connStr, int timeout)
    {
        ConnectionString = connStr;
        TimeoutSeconds = timeout;
    }
}

// 應用啟動時配置
SingletonProvider.Configure(() => 
    new DatabaseConnection("Server=.;Database=MyDB;Trusted_Connection=True;", 30));

// 之後直接使用
var conn = SingletonProvider.Instance;
Console.WriteLine($"連線字串: {conn.ConnectionString}");

使用範例:測試類別


// 一般類別:只需加上無參數建構子
public class TestClass
{
    private readonly string _createdTimestamp;

    public TestClass()
    {
        _createdTimestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
    }

    public void Write() => Console.WriteLine($"建立時間: {_createdTimestamp}");
}

// 客戶端程式碼
public class Program
{
    public static void Main()
    {
        // 多執行緒測試
        var tasks = Enumerable.Range(1, 100).Select(i => Task.Run(() =>
        {
            var instance = SingletonProvider.Instance;
            instance.Write();
        })).ToArray();

        Task.WaitAll(tasks);
        
        // 驗證所有執行緒取得同一實例
        Console.WriteLine("所有執行緒輸出的時間戳記應完全相同");
    }
}

原理解析:為什麼 Lazy 完美解決單例問題?

特性Lazy<T> 行為單例需求
執行緒安全預設 `LazyThreadSafetyMode.ExecutionAndPublication` 保證多執行緒環境下只建立一個實例
延遲初始化首次存取 `.Value` 時才建立避免啟動時不必要的建立開銷
例外快取建立失敗會快取例外,後續存取重擲避免重複嘗試建立失敗的實例
效能內部使用雙重檢查鎖定最佳化高併發下極低開銷

比較:各種單例實作方式

方式執行緒安全延遲初始化程式碼重用備註
靜態 readonly 欄位✅ (CLR 保證)❌ (型別載入時初始化)❌ 每類別重寫最簡單,適用大多數情況
雙重檢查鎖定✅ (需 volatile)✅❌ 每類別重寫易寫錯,.NET 已不推薦
Lazy<T> 包裝✅✅❌ 每類別重寫現代 .NET 推薦手寫方式
SingletonProvider<T>✅✅✅ 寫一次通用本文方案,最佳重用性
DI 容器 (Singleton 生命週期)✅✅✅ 架構級重用企業級應用首選

實務考量與限制

限制 1:泛型約束 `where T : new()`

  • 要求公用無參數建構子
  • 無法直接用於需參數建構的類別 (需進階版 `Configure`)
  • 無法用於 `abstract`、`interface`、`static` 類別

限制 2:繼承問題


public class Base { }
public class Derived : Base { }

// 這會建立兩個獨立單例!
var baseInstance = SingletonProvider.Instance;
var derivedInstance = SingletonProvider.Instance;
// baseInstance != derivedInstance (不同型別參數 = 不同靜態欄位)

限制 3:測試困難

  • 靜態單例難以 Mock/替換
  • 建議:核心業務邏輯依賴介面 (`ILogger`),單例只作為預設實作註冊到 DI 容器

限制 4:記憶體洩漏風險

  • 單例存活至應用程式結束
  • 若單例持有大量資源或事件訂閱未解除,會導致記憶體無法釋放

現代替代方案:依賴注入容器


// ASP.NET Core / .NET Generic Host 內建 DI
public interface ILogger
{
    void Log(string message);
}

public class ConsoleLogger : ILogger
{
    public void Log(string message) => Console.WriteLine($"[LOG] {message}");
}

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// 註冊為 Singleton 生命週期 (容器管理單例)
builder.Services.AddSingleton();

// 或註冊工廠委派
builder.Services.AddSingleton(sp => 
    new FileLogger("app.log", sp.GetRequiredService()));

// 控制器/服務透過建構子注入 (無單例樣板碼)
public class OrderService
{
    private readonly ILogger _logger;
    public OrderService(ILogger logger) => _logger = logger;
    
    public void Process() => _logger.Log("處理訂單...");
}

何時使用 SingletonProvider vs DI 容器?

場景推薦方案
ASP.NET Core / Worker Service / 現代 .NET 應用DI 容器 (AddSingleton)
Legacy WinForms / WPF / Console 應用 (無 DI)SingletonProvider<T>
函式庫/套件開發 (不想強制使用者用 DI)SingletonProvider<T> + 可選 DI 整合
單元測試專案介面 + Mock / 測試用 DI 容器
需支援參數化建構、重設、多租戶進階 SingletonProvider 或 DI

總結

C# 2.0 泛型讓 **`SingletonProvider`** 成為可能,將單例樣板碼從「每類別重寫」降維為「零樣板碼」。核心優勢:

  1. 消除重複:單例邏輯集中在一處,符合 DRY 原則
  2. 執行緒安全:借用 `Lazy` 內建的最佳化雙重檢查鎖定
  3. 延遲初始化:首次使用時才建立,節省啟動資源
  4. 彈性擴充:進階版支援參數化建構、工廠委派

但在現代 .NET 架構中,**依賴注入容器的 Singleton 生命週期** 才是企業級應用的標準解法。`SingletonProvider` 適合無 DI 環境、函式庫開發、快速原型、Legacy 專案遷移等場景。理解兩者關係,才能在適當場合做出最佳架構決策。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved