Singletonの種類を知ってる限り全部整理してみました。
これ以外に何かあったらいつでも教えてください!
目次
1. Pure Singleton
2. Pure Singleton + Thread-safe
3. Pure Singleton + Thread-safe (using Lazy)
4. Pure Generic Singleton + Thread-safe (using Lazy)
5. Non-Persistent MonoBehaviour Generic Singleton
6. Persistent MonoBehaviour Generic Singleton
7. Persistent MonoBehaviour Generic Singleton + seal Awake
8. Persistent MonoBehaviour RegulatorSingleton
1. Pure Singleton
MonoBehaviourを継承せず、Unityのライフサイクルに依存しない一般的なクラスでの実装です。
大体はMonoBehaviourがあるSingletonを使う時が多いですが、なんか簡単なオンライン要素があるハイパーカジュアルゲームの場合だと開発コストやサーバー側と同じコードを使いたい場合があるのでそうするとPureなSingletonの需要があります。
public sealed class ScoreManager
{
private static ScoreManager _instance;
public static ScoreManager Instance
{
get
{
if (_instance == null)
{
_instance = new ScoreManager();
}
return _instance;
}
}
private ScoreManager()
{
// Init
}
}
2. Pure Singleton + Thread-safe
そうなるとサーバーはThread-safeを担保するためにLockを使います。
public sealed class ScoreManager
{
private static ScoreManager _instance;
private static readonly object _lock = new object();
public static ScoreManager Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
if (_instance == null)
{
_instance = new ScoreManager();
}
}
}
return _instance;
}
}
private ScoreManager()
{
// Init
}
}
3. Pure Singleton + Thread-safe(using Lazy)
上のコードをLazyを使ったらもっどモダンなコードにできます。
public sealed class ScoreManager
{
// Lazyは基本Thread-Safe
private static readonly Lazy<ScoreManager> _lazy =
new Lazy<ScoreManager>(() => new ScoreManager());
public static ScoreManager Instance => _lazy.Value;
private ScoreManager()
{
// Init
}
}
C#でサーバーを実装すき、Lockはいつもバグが起きやすいものなんでこっちの方がもっとおすすめです
そして上のLockはUnityで使うのは色々ややこしいバグが起きやすいのでLazyの方がサーバーと完全に同じコードでいいです。(まとめてDLL化して共有したり)
4. Pure Generic Singleton + Thread-safe(using Lazy)
Genericでもうちょっと一般的に使えるPure Generic Singletonも実装できます。
でもPureなC#クラスではnew T()を呼ばなきゃいけないのにPrivate Constructのクラスはwhere T : new()でインスタンスを作れない問題があります。
なのでReflectionを使います。Overheadはありますが何回も呼ばれるものじゃないのでそんなに意味ないです。
ナノ最適化強迫症は病気です。
public abstract class Singleton<T> where T : class
{
private static readonly Lazy<T> _lazy = new Lazy<T>(() =>
{
// Tの private Constructを探す
var constructor = typeof(T).GetConstructor(
BindingFlags.Instance | BindingFlags.NonPublic,
null, Type.EmptyTypes, null);
if (constructor == null)
{
throw new Exception($"No private constructor found for {typeof(T).Name}.");
}
return (T)constructor.Invoke(null);
});
public static T Instance => _lazy.Value;
}
5. Non-Persistent MonoBehaviour Generic Singleton
Componentとして存在するMonoBehaviour Singletonです。シーンが変わったらなくなります。
これは1人開発の時は使っても大丈夫です。
例えばStage SceneだけのSingletonが欲しいからStageManagerを作るとき使います。
でもチーム開発になるとこれはLobby Sceneでも呼べるしいろんなシーンから呼べれるからどんな問題になるか恐ろしいです。
チーム開発の時はあんまり使わない方がいいです。
public class Singleton<T> : MonoBehaviour where T : Component
{
protected static T instance;
public static T Instance
{
get {
if (instance == null)
{
instance = FindAnyObjectByType<T>();
if (instance == null)
{
GameObject go = new GameObject(typeof(T).Name);
instance = go.AddComponent<T>();
}
}
return instance;
}
}
}
6. Persistent MonoBehaviour Generic Singleton
一番一般的なSingletonです。シーンが変わっても生きてます。
public class PersistentSingleton<T> : MonoBehaviour where T : Component
{
protected static T instance;
public static T Instance
{
get
{
if (instance == null)
{
instance = FindAnyObjectByType<T>();
if (instance == null)
{
var go = new GameObject(typeof(T).Name);
instance = go.AddComponent<T>();
}
}
return instance;
}
}
protected virtual void Awake()
{
InitializeSingleton();
}
protected virtual void InitializeSingleton()
{
if (instance == null)
{
instance = this as T;
DontDestroyOnLoad(gameObject);
}
else
{
if (instance != this)
{
Destroy(gameObject);
}
}
}
}
7. Persistent MonoBehaviour Generic Singleton + seal Awake
上のコードで何か問題を見つけましたか?
Awakeをもし子供クラスでoverrideしてBase.Awakeを実行しなかったらInitializeSingleton()が呼べなくてバグります。
AwakeをSealして問題を改善しました。
public class PersistentSingleton<T> : MonoBehaviour where T : Component
{
protected static T instance;
public static T Instance
{
get
{
if (instance == null)
{
instance = FindAnyObjectByType<T>();
if (instance == null)
{
var go = new GameObject(typeof(T).Name);
instance = go.AddComponent<T>();
}
}
return instance;
}
}
// AwakeをSeal
protected void Awake()
{
InitializeSingleton();
// Awakeの代わりに使うMethodを呼ぶ
OnAwake();
}
protected virtual void InitializeSingleton()
{
if (instance == null)
{
instance = this as T;
DontDestroyOnLoad(gameObject);
}
else if (instance != this)
{
Destroy(gameObject);
}
}
protected virtual void OnAwake() { }
}
7. Persistent MonoBehaviour RegulatorSingleton
今までのSingletonは過去のものが生き残るSingletonですがこれは新しいのを残して過去のインスタンスはなくすSingletonです。
シーンごとにシングルトンを配置し、そのシーン内のものを優先したい場合に非常に有効な手法です。
例えば'Field Scene'では平和なBGM設定のAudioManager Prefabがあるし, 'Boss Scene'では 緊迫したBGM設定のAudioManager Prefabがあります。
一般的なSingletonの場合 :
'Field Scene'から'Boss Scene'へ移動しても、'Field Scene'のAudioManagerが破棄されずに維持されます。
なので'Boss Scene'に配置されていた(新しい設定の)AudioManagerの方が削除されてしまい、'Field Scene'の平和な設定がそのまま残ってしまうという問題が発生します。
RegulatorSingletonの場合 :
'Boss Scene'に入った瞬間、そのSceneにある新しいAudioManagerが既存の古いAudioManagerを破棄し主導権を握ります。
これにより、各シーンに最適化された設定を即座に適用することが可能になります。
public class RegulatorSingleton<T> : MonoBehaviour where T : Component {
protected static T instance;
public static T Instance {
get {
if (instance == null) {
instance = FindFirstObjectByType<T>();
if (instance == null) {
GameObject obj = new GameObject(typeof(T).Name);
instance = obj.AddComponent<T>();
}
}
return instance;
}
}
[SerializeField] protected float initializationTime;
protected virtual void Awake() {
initializationTime = Time.time;
DontDestroyOnLoad(gameObject);
// 3. インスタンス整理
T[] oldInstances = FindObjectsByType<T>(FindObjectsSortMode.None);
foreach (T old in oldInstances) {
if (old.gameObject == gameObject) continue;
var controller = old as RegulatorSingleton<T>;
if (controller != null && controller.initializationTime < initializationTime) {
Destroy(old.gameObject);
}
}
if (instance == null) {
instance = this as T;
}
}
}