目的保證class只有一個object instance,並且提供單一存取點。
時機
- 類別只能有一個實體,且需要提供方便的單一窗口給外界使用。
- 類別也能以繼承的方式來擴充介面,不須修改原來的類別。
遊戲中通常有許多管理類型的class,並且絕大多數的狀況下只需要一個,這些管理器需要提供單一存取點給其他人來用。這時候就適合使用Singleton來解決此問題。不過在遊戲程式中,管理類型的class大都不會被繼承,尤其是愈高層次的遊戲邏輯,愈難有被繼承的情況,此時就得思考是否真的該設計為singleton。
但在Unity中,有些狀況會是繼承Component、Behaviour或MonoBehaviour之類的Unity class,這類class基於Unity的機制,禁止定義constructor,因此實作手法和一般有所差異。
一般範例,方法一
public class Manager
{
private static Manager s_Instance;
public static Manager Instance
{
get
{
if (s_Instance == null)
s_Instance = new Manager();
return s_Instance;
}
}
private Manager() { }
}
一般範例,方法二
public class Manager
{
private static readonly Manager s_Instance = new Manager();
public static Manager Instance
{
get
{
return s_Instance;
}
}
private Manager() { }
}
Unity Component範例
public class Manager : MonoBehaviour
{
private static Manager s_Instance;
public static Manager Instance
{
get
{
if (s_Instance == null)
{
s_Instance = FindObjectOfType(typeof(Manager)) as Manager;
if (s_Instance == null)
{
GameObject go = new GameObject("Manager");
s_Instance = go.AddComponent<Manager>();
}
}
return s_Instance;
}
}
void Start()
{
if (Instance != this) Destroy(this);
}
}
由於與Unity核心有關的程式都是在main thread,而那些管理器若要在遊戲中被使用,也只能自main thread產生,在這情況下沒有多執行緒會遇到的問題。
※WWW及Rendering則真的是多執行緒執行,WWW會有背景執行緒在執行下載任務。
進階Singleton
一種作法是採用註冊表的方式,不使用Instance窮舉所有Singleton class,而是讓這些class自行向註冊表登記,但有三個缺點,一是取得Instance還得轉型才能使用。二是在註冊時必須窮舉所有Singleton,使用起來有些彆扭。三是這麼會喪失延遲初始化(Lazy initialization)的效果,但遊戲中設計的singleton通常都一定會用到,因此這缺點不是很重要。
(尚未驗證)
public abstract class Singleton
{
private static Singleton s_Instance;
private static Dictionary<Type, Singleton> s_Registry =
new Dictionary<Type, Singleton>();
protected static Singleton Lookup(Type type)
{
if (type == null) return null;
if (s_Registry.ContainsKey(type)) return s_Registry[type];
return null;
}
public static void Register(Singleton singleton)
{
if (singleton == null) return;
if (s_Registry.ContainsKey(singleton.GetType())) return;
s_Registry.Add(singleton.GetType(), singleton);
}
public static Singleton GetInstance(Type type)
{
if (type == null) return null;
if (s_Instance == null)
s_Instance = Lookup(type);
return s_Instance;
}
}
public class MySingleton : Singleton
{
private static MySingleton s_MySingleton = new MySingleton();
private MySingleton()
{
Singleton.Register(this);
}
}
改進版(待修正)
省去轉型與傳遞Type的麻煩。
public class Singleton<T> where T : Singleton<T>
{
private static T s_Instance;
private static Dictionary<Type, object> s_Registry =
new Dictionary<Type, object>();
protected static T Lookup(Type type)
{
if (type == null) return null;
if (s_Registry.ContainsKey(type))
return s_Registry[type] as T;
return null;
}
public static void Register(T singleton)
{
if (singleton == null) return;
if (s_Registry.ContainsKey(singleton.GetType())) return;
s_Registry.Add(singleton.GetType(), singleton);
}
public static T GetInstance()
{
if (s_Instance == null)
s_Instance = Lookup(typeof(T));
return s_Instance;
}
}
public class MySingleton : Singleton<MySingleton>
{
private static MySingleton s_MySingleton = new MySingleton();
private MySingleton()
{
Singleton<MySingleton>.Register(this);
}
}
但若遇到多執行緒問題,該怎麼辦呢?
C#的做法很簡單,加個sync root鎖住,並指示編譯器不要對s_Instance最佳化即可。
public class Manager
{
private static volatile Manager s_Instance;
private static object s_SyncRoot = new object();
public static Manager Instance
{
get
{
if (s_Instance == null)
{
lock (s_SyncRoot)
{
if (s_Instance)
s_Instance = new Manager();
}
}
return s_Instance;
}
}
private Manager() { }
}
Unity Component Singleton可能會遇到的其他狀況
有一種可能是會遇到當遊戲程式結束時,某一singleton已經被Destroy了,但在其他地方卻又呼叫了該singleton的Instance,於是又產生了該singleton的Instance,這時候該怎麼辦呢?
- 不得在所有OnDestroy()、finalizer (destructor)使用任何singleton的Instance,此方法需要規範程式人員。
- 清除Singleton刪除時,應該要Destroy field,而不是調用Instance,也不可以在finalizer才清除。
- 給予遊戲程式狀態,當遊戲程式進入結束狀態時,所有Instance均會無效,但這麼做會導致singleton與遊戲邏輯產生相依性問題。
其他 (static class)
若有一種功能確定不應該被繼承擴充,也確定在遊戲中是唯一,也確定功能是經常被用到的,那麼這個情況就不應該考慮使用Singleton,而應該是直接設計成static class。例如Unity的Resources, AssetDatabase等class。此狀況就不符合使用singleton的時機。
參考文獻