1. 程式人生 > 程式設計 >解析C#設計模式之單例模式

解析C#設計模式之單例模式

單例模式(Singleton),故名思議就是說在整個應用程式中,某一物件的例項只應該存在一個。比如,一個類載入資料庫中的資料到記憶體中以提供只讀資料,這就很適合使用單例模式,因為沒有必要在記憶體中載入多份相同的資料,另外,有些情況下不允許記憶體中存在多分份相同的資料,比如資料過大,記憶體容不下兩份相同資料等等。

約定單例模式(Singleton by Convention)

這種方式有點“Too simple,Sometimes naïve”,他就是提示使用者,我是單例,不要重複初始化我,比如:

public class Database
{
  /// <summary>
  /// 警告,這是單例,不要初始化多次,否則,後果自負.
  /// </summary>
  public Database() {}
};

一種情況是,根本不會注意到這個提示,其次是在很多時候,這些初始化是偷偷摸摸無意中發生的,比如通過反射,通過工廠產生(Activator.CreateInstance),通過注入等等,雖然有一個“約定大於配置”,但是這裡不使用。

單例模式最常見的想法是提供一個全域性的,靜態的物件。

public static class Globals
{
  public static Database Database = new Database();
}

這種方式並沒有很安全。這個並沒有阻止使用者在其他地方new Database,並且,使用者可能並不知道有一個Globals類,裡面有個Database單例。

經典的實現方式

唯一的方式阻止使用者例項化物件是將建構函式變成私有的,並且提供方法或者屬性返回唯一的內部物件。

public class Database
{
  private Database() { ... }
  public static Database Instance { get; } = new Database();
}

現在將建構函式設定為了私有,當然設為私有依舊可以通過反射繼續呼叫,但是這畢竟需要額外操作,這已經可以阻止大部分使用者直接例項化了。通過將例項定義為static靜態, 使得其生命週期延長至應用程式執行期間。

延遲初始化

以上方法執行緒安全,但是因為是靜態屬性,在類的所有例項建立之前,或者任何靜態成員訪問之前就會初始化,並且在每個AppDomain裡都只會初始化一次。

如何實現延遲初始化,即將單例物件的構造推遲到應用程式首次請求該物件進行時,如果應用程式永遠不請求物件,則物件永遠不會構造。在之前,可以使用double check方式。其實要實現正確的double check還是有些問題需要注意,比如上面這個例子,第一版可能這麼寫,用一個鎖。

public class Database
{
  private static Database db;
  private static object olock = new object();
  private Database()
  { }

  public static Database GetInstance()
  {
    lock (olock)
    {
      if (db == null)
      {
        db = new Database();
      }
      return db;
    }
  }
}

這雖然執行緒安全,但是每次訪問GetInstance,不論物件是否已經建立,都需要獲取然後釋放鎖,比較消耗資源,所以,在外面再加一層判斷。

public class Database
{
  private static Database db;
  private static object olock = new object();
  private Database()
  { }

  public static Database GetInstance()
  {
    if (db == null)
    {
      lock (olock)
      {
        if (db == null)
        {
          db = new Database();
        }
      }
    }
    return db;
  }
}

在訪問物件之前判斷是否已經初始化,如果初始化直接返回,這樣就避免了一次對鎖的訪問。But,這裡仍然存在問題。假設Database初始化起來耗時,當執行緒A獲得鎖正在對db進行初始化的時候,執行緒B在最外層判斷db是否為空,這個時候,執行緒A正在初始化db,有可能只初始化了部分,這個時候db就可能不為空,直接返回了沒有完全初始化完全的物件,這可能導致執行緒B崩潰。

解決方式是,將物件儲存到臨時變數中,然後以原子寫的方式儲存到db中,如下

public class Database
{
  private static Database db;
  private static object olock = new object();
  private Database()
  { }

  public static Database GetInstance()
  {
    if (db == null)
    {
      lock (olock)
      {
        if (db == null)
        {
          var temp = new Database();
          Volatile.Write(ref db,temp);
        }
      }
    }
    return db;
  }
}

非常繁瑣,雖然實現了延遲初始化,但是跟開頭的靜態欄位對比,複雜太多,而且一不小心就會寫錯。雖然可以簡化為:

public static Database GetInstance()
{
  if (db == null)
  {
    var temp = new Database();
    Interlocked.CompareExchange(ref db,temp,null);
  }
  return db;
}

該方法看起來沒有用鎖,temp物件有可能會被初始化兩次,但是在將temp寫入到db的時候,Interlock.CompareExchange會保證只會有1個物件正確被寫入到db,沒有被寫入的temp物件會被垃圾回收,這種方式速度比上述的double check要快。但仍然需要學習成本。幸好,C#提供了Lazy方法:

private static Lazy<Database> db = new Lazy<Database>(() => new Database(),true);
public static Database GetInstance() => db.Value;

簡單且完美。

依賴注入與單例模式

前面的單例模式其實是一種程式碼侵入的做法,就是要想一個原本沒有實現單例的程式碼要實現單例,需要修改程式碼實現,並且這個程式碼還容易出錯。有些人認為單例模式的唯一正確的做法就是在IOC依賴注入中,這樣不需要修改原始碼,通過依賴注入框架來實現依賴注入,在統一的入口,統一的管理生命週期,在ASP.NET Core MVC中,在Startup的ConfigureServices程式碼中:

services.AddSingleton<Database>();

或者加入需要用IDatabase的地方,要用Database單例的話:

services.AddSingleton<IDatabase,Database>();

在ASP.NET Core MVC的後續程式碼中,只要用到IDatabase的地方,就會用Database的單例來實現,不需要我們在Database內做任何修改。在使用的時候,只需要引用IServiceProvider接口裡的GetService方法,IServiceProvider是由ASP.NET Core MVC的IOC框架直接提供,不需要特別處理:

public XXXController(IServiceProvider serviceProvider)
{
   var db = serviceProvider.GetService<IDatabase>();
}

單態模式(Monostate)

單態模式是單例模式的一個變種,它是一個普通的類,但是其行為和表現就像單例模式。

比如我們在對一個公司的人員結構進行建模,一個典型的公司一般只會有一個CEO,

public class ChiefExecutiveOfficer
{
  private static string name;
  private static int age;

  public string Name
  {
    get => name;
    set => name = value;
  }

  public int Age
  {
    get => age;
    set => age = value;
  }
}

這個類的屬性有get,set,但是其背後的私有欄位都是靜態的。所以不管這個ChiefExecutiveOfficer例項化多少次,其內部引用的都是同一個資料。比如,可以例項化兩個物件,但是其內部的內容是一模一樣的。

單態模式簡單,但是容易引起混亂,所以要想簡單,要想實現單例效果,最好還是使用IOC這種依賴注入的框架,讓它來幫助我們來管理例項及其生命週期。

以上就是解析C#設計模式之單例模式的詳細內容,更多關於c# 單例模式的資料請關注我們其它相關文章!