引子:類也需要計畫生育#
小菜寫了一個 MDI 應用,「工具箱」窗體應該最多只出現一個。但他在按鈕事件裡每次 new FormToolbox(),於是按越多次就跳越多個工具箱。
這個類超生了——大鳥用「計畫生育」的比喻形容這個情境。
第一輪:判斷是否為 null#
把 FormToolbox 提升為類別變數,在開啟前判斷:
private FormToolbox ftb;
private void ToolStripMenuItemToolbox_Click(object sender, EventArgs e)
{
if (ftb == null)
{
ftb = new FormToolbox();
ftb.MdiParent = this;
ftb.Show();
}
}啟動工具箱後關閉再點擊按鈕,會出現「已經 Disposed」異常——關閉只是 Dispose,不會把
ftb設為 null。
修正:增加 IsDisposed 判斷。
if (ftb == null || ftb.IsDisposed) { ... }並提取為 openToolbox() 方法給多個地方呼叫,避免複製貼上。
第二輪:誰負責判斷?#
父母的計畫生育責任在他們自己,不是別人。
同樣道理:「工具箱」是否實例化過,應由「工具箱自己」判斷,而不是由
Form1來判斷。
實作方式:
- 把建構子改為
private,外界不能new - 提供一個
public static GetInstance()方法,由類別自己決定回傳已存在的或新建一個
public partial class FormToolbox : Form
{
private static FormToolbox ftb = null;
private FormToolbox()
{
InitializeComponent();
}
public static FormToolbox GetInstance()
{
if (ftb == null || ftb.IsDisposed)
{
ftb = new FormToolbox();
ftb.MdiParent = Form1.ActiveForm;
}
return ftb;
}
}客戶端:
private void ToolStripMenuItemToolbox_Click(object sender, EventArgs e)
=> FormToolbox.GetInstance().Show();
private void toolStripButton1_Click(object sender, EventArgs e)
=> FormToolbox.GetInstance().Show();單例模式#
單例模式(Singleton Pattern):保證一個類僅有一個實例,並提供一個訪問它的全域訪問點。[DP]
結構#
classDiagram
class Singleton {
-Singleton instance$
-Singleton()
+GetInstance() Singleton$
}class Singleton
{
private static Singleton instance;
private Singleton() { }
public static Singleton GetInstance()
{
if (instance == null)
instance = new Singleton();
return instance;
}
}全域變數能讓物件被訪問,但不能阻止實例化多個物件。
最好的辦法:讓類別自身負責保存它的唯一實例。
單例 vs. 實用類#
兩者都常用私有建構子,但:
- 實用類(如 .NET 的
Math):不保存狀態,只提供靜態方法/屬性;不能被繼承多型 - 單例:有狀態;可以被子類繼承
多執行緒下的單例#
多執行緒環境下,多個執行緒同時呼叫
GetInstance(),可能造成創建多個實例。
加鎖(lock):
class Singleton
{
private static Singleton instance;
private static readonly object syncRoot = new object();
private Singleton() { }
public static Singleton GetInstance()
{
lock (syncRoot)
{
if (instance == null)
instance = new Singleton();
}
return instance;
}
}不能
lock(instance)——加鎖時instance可能還沒被建立,沒辦法對null加鎖。因此用一個獨立的
syncRoot物件作為鎖。
雙重鎖定(Double-Check Locking)#
每次呼叫都加鎖會影響效能。改良為雙重鎖定:
public static Singleton GetInstance()
{
if (instance == null) // 第一重檢查
{
lock (syncRoot)
{
if (instance == null) // 第二重檢查
instance = new Singleton();
}
}
return instance;
}為什麼需要兩重檢查?
- 對於已存在的情況,第一重判斷直接回傳,不用進入鎖
- 當
instance為 null 且兩個執行緒同時通過第一重時,lock只讓一個進入;第二重判斷防止第二個執行緒在第一個執行緒建立後再次建立實例
靜態初始化(餓漢式)#
C# 與公共語言運行庫提供了一種靜態初始化方式,不需顯式編寫執行緒安全程式碼即可解決多執行緒問題:
public sealed class Singleton
{
private static readonly Singleton instance = new Singleton();
private Singleton() { }
public static Singleton GetInstance() => instance;
}sealed:阻止派生(派生可能增加實例)readonly:意味著只能在靜態初始化期間或類別建構函式中分配變數
餓漢式(靜態初始化):類別載入時就實例化,提前佔用系統資源
懶漢式(前面的雙重鎖定):第一次被引用時才實例化,面臨多執行緒安全問題,需要雙重鎖定處理
實際選用視需求而定。對 C# 而言,餓漢式通常已足夠。