解析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# 單例模式的資料請關注我們其它相關文章!