Unity シングルトン モードのベスト プラクティス (コード付き)


序章

Unity でシングルトンを実装するためのいくつかのソリューションを体系的に整理します。

実装ソリューションは、次の 2 つのシナリオに対して提供されます。

  1. Pure C#実装(7種類)

  1. MonoBehaviourから継承(3種類)

さまざまなソリューションの長所と短所が分析され、推奨されるエレガントなソリューション、つまり Unity でシングルトン モードを実現するための最適なソリューションが提供されます。

Unity では、MonoBehaviour を継承するクラスがシングルトンを実装する方法は、MonoBehaviour を継承しないシングルトンがシングルトンを実装する方法とは異なります。

この点を理解していない人がいて、C# の実装の一部を MonoBehaviour サブクラスにコピーしましたが、機能せず、意味のないことを書いていました。

コンストラクターにおける MonoBehaviour と純粋な C# の違いについては、私の回答を参照してください。

MonoBehaviour ソリューションを使用するか、純粋な C# ソリューションを使用するかについては、純粋にニーズによって異なります。

純粋な C# 実装はパフォーマンスが優れていますが、コンポーネントをマウントできないため、デバッグが面倒になります。

純粋な C# はシングルトン モードを実装します (Mono 動作から継承されません)。

ありがたい

コンテンツのこの部分は主に、「C# In Depth」のシングルトン実装の章を参照しています。

私の理解を補足し、一般的な実装を追加しただけです

文章

「C# In Depth」では、C# でシングルトンを実装するための 6 つのソリューションを、最もエレガントでないものから最もエレガントなものまで並べて提供します。

第 6 バージョンは圧倒的なアドバンテージであり、Tukuai は第 6 バージョンに直接ジャンプできます。

これら 6 つのプログラムには、次の 4 つの共通機能があります。

  • パラメーターを持たないプライベート コンストラクターは 1 つだけです。これにより、他のクラスがそれを初期化できないことが保証されます (シングルトン設計パターンが壊れる可能性があります)。

  • 継承はデザイン パターンの破壊にもつながります。したがって、封印する必要があります。必須ではありませんが、お勧めします。JIT の最適化に役立つ可能性があります。

  • 静的変数 _Instance があります。

  • シングルトンを取得するためのパブリック インスタンス メソッドがあります。

最初のバージョン - スレッドセーフではありません

このコードは最も単純な遅延スタイルです。

// Bad code! Do not use!
public sealed class Singleton
{
    private static Singleton instance = null;
    private Singleton()
    {
    }
    public static Singleton Instance
    {
        get
        {
            if (instance == null)
            {
                instance = new Singleton();
            }
            return instance;
        }
    }
}

スレッドセーフではありません。(instance == null) マルチスレッドでの判定が不正確となり、インスタンスごとに生成されてしまい、シングルトンパターンに違反します。

2 番目のバージョン - シンプルなスレッドセーフな書き込み

このプログラムはロックを追加します。

public sealed class Singleton
{
    private static Singleton instance = null;
    private static readonly object padlock = new object();
    Singleton()
    {
    }
    public static Singleton Instance
    {
        get
        {
            lock (padlock)
            {
                if (instance == null)
                {
                    instance = new Singleton();
                }
                return instance;
            }
        }
    }
}

利点は、スレッドが安全であることですが、get がロックを取得するたびにパフォーマンスが影響を受けることです。

3 番目の解決策 - ダブルチェックとロックを使用してスレッドの安全性を実現します

この解決策はインターネット上で広く推奨されていますが、作成者は直接これを悪いコードとしてラベル付けし、推奨していません。

public sealed class Singleton
{
    private static Singleton instance = null;
    private static readonly object padlock = new object();
    Singleton()
    {
    }
    public static Singleton Instance
    {
        get
        {
            if (instance == null)
            {
                lock (padlock)
                {
                    if (instance == null)
                    {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
}

このスキームは、(instance == null ) 判定を追加することでスキーム 2 を最適化し、パフォーマンスを向上させます。しかし、彼には 4 つの欠点があります。

  • 彼は Java では働いていません。

  • もうメモリの壁はありません。.NET 2.0 メモリ モデルでは安全かもしれませんが、私はこうした強力なセマンティクスには依存したくありません。(質問です)

  • 間違えやすいです。スキーマは上記のコードとまったく同じである必要があります。破壊的な変更はパフォーマンスや正確性に影響を与える可能性があります。(ジェネリックを使用すればこの問題は回避できます。使用しない場合はコピーするだけで問題は大きくなります。)

  • そのパフォーマンスはまだ後者の実装ほど良くありません。(質問です)

4 番目の解決策 - 飢えた中国スタイル (遅延初期化はありませんが、ロックなしでスレッドセーフです)

前の 3 つの解決策はすべて遅延的です。本質的にスレッド安全ではありません。スレッド安全のためにロックを使用します。ロックを使用すると追加のオーバーヘッドが発生しますが、これは避けられません。実際、Hungry Chinese スタイルを直接使用するのは非常に良いことです。

public sealed class Singleton
{
    private static readonly Singleton instance = new Singleton();
    // Explicit static constructor to tell C# compiler
    // not to mark type as beforefieldinit
    static Singleton()
    {
    }
    private Singleton()
    {
    }
    public static Singleton Instance
    {
        get
        {
            return instance;
        }
    }
}

ご覧のとおり、コードは非常に単純です。

C# の静的コンストラクターは、そのクラスがインスタンスの作成に使用される場合、または参照される静的メンバーがある場合にのみ呼び出されます。

明らかに、このスキームは、上記で追加されたチェックを備えた 2 番目および 3 番目のスキームよりも高速です。しかし:

  • 他のソリューションほど「怠惰」(つまり、遅延)ではありません。

特に他の静的メソッドがある場合、他の静的メソッドを呼び出すとインスタンスも生成されます。(次の解決策でこの問題を解決できます)

  • 1 つの静的コンストラクターが別の静的コンストラクターを呼び出すと、複雑な問題が発生します。

詳細については、.NET 仕様を参照してください。この質問はおそらく「刺さる」ものではありませんが、ループ内の静的コンストラクターの実行順序を確認する価値はあります。

5 番目の解決策 - 完全な遅延スタイル (完全な遅延初期化)

public sealed class Singleton
{
    private Singleton()
    {
    }
    public static Singleton Instance { get { return Nested.instance; } }
    private class Nested
    {
        // Explicit static constructor to tell C# compiler
        // not to mark type as beforefieldinit
        static Nested()
        {
        }
        internal static readonly Singleton instance = new Singleton();
    }
}

このソリューションは、内部クラスをネストして実装します。効果の点では、4 番目の解決策よりも少し優れていますが、非常に型破りです。

6 番目の解決策 - .NET 4 の遅延タイプを使用する

.NET 4 以降を使用している場合は、System.Lazy を使用してオブジェクトの遅延初期化を簡単に実装できます。必要なのは、(Delegate) を渡してコンストラクターを呼び出すことだけです。これは、ラムダ式を使用して簡単に実行できます。

public sealed class Singleton
{
    private static readonly Lazy<Singleton> lazy =
        new Lazy<Singleton>(() => new Singleton());
    public static Singleton Instance { get { return lazy.Value; } }
    private Singleton()
    {
    }
}

简单又高效。如果需要,它还允许你使用isValueCreated属性来检查实例是否已经创建好。

### 关于性能与延迟初始化
在许多情况下,其实你并不需要完全的延迟初始化(即你不需要那么追求懒汉式)——除非你的类的构造非常耗时或者有一些别的副作用。
クイックファクト - Unity で使用される .NET バージョン
  • Unity 2017.1 より前は、Unity で使用される .NET バージョンは .NET 3 でした。この新しい構文は使用できません

  • Unity 2017.1~Unity 2018.1、試用版として.NET 4.1を導入

  • Unity 2018.1~現在は.NET 4.xが標準構成となっています。(.NET Standard 2.1と互換性があります)

現在使用されている .NET バージョンは、Unity の API 互換性レベル* リストで確認できます。

この方法を使用する前に、.NET のバージョンを確認してください。

「C# の深さ」の著者からの結論

『Depth in C#』の著者が推奨する解決策は解決策 4 で、これはシンプルな空腹の中華スタイルです。

初期化をトリガーせずに他の静的メソッドを呼び出す必要がない限り、通常はオプション 4 を使用します。

解決策 5 は洗練されていますが、2 や 4 よりも複雑で、メリットもほとんどありません。

.NET 4 以降を使用している場合は、解決策 6 が最適な解決策です。

それでも、現在の傾向はオプション 4、つまり習慣に従うことを使用することです。

ただし、経験の浅い開発者と作業している場合は、シンプルで一般的に使用されるパターンとしてオプション 6 を使用するでしょう。

選択肢1はゴミです。

オプション 3 は使用されません。オプション 3 よりもオプション 2 を使用したいと思います。

また、解決策 3 の賢さについても批判しており、ロックのオーバーヘッドはそれほど大きくなく、解決策 3 の書き方が最適化されているだけのようです。

彼はオーバーヘッドを検証するためにテスト コードを作成しましたが、両者の差は非常にわずかです。

ロックは高価であるというのはよくある誤った情報です。

本当にコストがかかると思われる場合は、呼び出しループの外側にインスタンスを保存するか、単にスキーム 5 を使用することができます。

つまり、4=6>5>2>3=1

私の推奨事項 - Unity でどの実装を使用するか

Unity 開発には Unity 開発の特徴がまだ残っています。

Unity でマルチスレッドを使用しない場合、Mono コードは元々シングルスレッドであるため、マルチスレッドの代わりにコルーチンを使用することが公式に推奨されています。

また、遅延初期化はゲーム開発にとって良いことではない可能性があります。

プレイヤーエクスペリエンスの観点から見ると、フレーム落ちは起動の遅さや読み込みの遅さよりも悪いです。

したがって、オプション 3 はほとんどの場合お勧めできません。マルチスレッドを使用しないのに、スレッドの安全性のために追加料金を支払い、それを最適化したと言い続けることになります...

オプション 6 はローリングアドバンテージです

もちろん、古いバージョンの .NET の場合は、オプション 7 を検討できると思います。オプション 3 はジェネリックスを使用して実装されます。

それでも、スキーム 7 はスキーム 6 やスキーム 4 よりも悪いです。

したがって、私の優先順位は次のとおりです。

スキーム 6[遅延文法の実装] > スキーム 4 (シンプルなハングリー スタイル) > スキーム 7 (ジェネリックスによって実装されたスレッドセーフな遅延スタイル) > 1 (マルチスレッドを記述しない場合はスレッド セーフを考慮しない) > 5>2> 3

具体的には、ニーズに応じて最適な実装というものはなく、最も適切なものがあるだけです

オプション 1 でこれほど上位にランクすることは議論の余地があるかもしれません。

私の要点は、マルチスレッドを使用しない場合は、マルチスレッド用に高価な最適化を追加すべきではないということです。これはネガティブな最適化です。

信じないでください。Unity ソース コードで純粋な C# でシングルトンを実装する方法を確認できます。

Unity ソースコードはどのようにしてシングルトンを実現するのか

実際、Unity はこのようなシングルトン ジェネリックを提供しています。 [ScriptableSingleton](

https://docs.unity3d.com/cn/2021.3/ScriptReference/ScriptableSingleton_1.html)、MonoBehaviourから継承せず、純粋な C# で実装されます。

ScriptableSingleton を使用すると、エディターで「マネージャー」タイプを作成できます。

ScriptableSingleton から派生したクラスでは、追加したシリアル化されたデータは、エディターがアセンブリを再読み込みした後も有効になります。

クラスで FilePathAttribute を使用すると、シリアル化されたデータは Unity セッション間で保持されます。

public class ScriptableSingleton<T> : ScriptableObject where T : ScriptableObject
{
    static T s_Instance;
    public static T instance
    {
        get
        {
            if (s_Instance == null)
                CreateAndLoad();
            return s_Instance;
        }
    }
    // On domain reload ScriptableObject objects gets reconstructed from a backup. We therefore set the s_Instance here
    protected ScriptableSingleton()
    {
        if (s_Instance != null)
        {
            Debug.LogError("ScriptableSingleton already exists. Did you query the singleton in a constructor?");
        }
        else
        {
            object casted = this;
            s_Instance = casted as T;
            System.Diagnostics.Debug.Assert(s_Instance != null);
        }
    }
    private static void CreateAndLoad()
    {
        // 一系列初始化代码
    }
    //其他代码……
}

ああ、それは選択肢 1 です。

とにかく、これが 7 番目の解決策です - 解決策 3 の一般的な実装です。

7 番目のスキーム - スキーム 3 の一般的な実装

public class SingletonV7<T> where T : class
{
    private static T _Instance;
    private static readonly object padlock = new object();
    public static T Instance
    {
        get
        {
            if (null == _Instance)
            {
                lock (padlock)
                {
                    if (null == _Instance)
                    {
                        _Instance = Activator.CreateInstance(typeof(T), true) as T;
                    }
                }
            }
            return _Instance;
        }
    }

}

純粋な C# の実装について説明した後、MonoBehaviour の継承の実装を見てみましょう。

シーンにマウントする必要がない場合は、純粋な C# 実装をお勧めします。

シーン内でシングルトンをハングする必要がある場合は、MonoBehaviour から継承したシングルトンを使用できます。


MonoBehavior から継承されたシングルトン

MonoBehavior シングルトンから継承されたいくつかの小さな問題

MonoBehaviour からの継承の主な問題は、呼び出し元が AddComponent を通じて新しいシングルトン オブジェクトを追加するのを防ぐことができないことです。

これは、シングルトンの設計原則にある程度違反しています。

シングルトンの一意性を維持するには、新しく生成されたコンポーネントを Destroy するしかありませんが、ここにはオーバーヘッドが発生し、純粋な C# 実装ほど優れたものではありません。

ありがたい

この部分では主にこの 2 つの記事を参照します。

最も基本的な解決策

using UnityEngine;
public class SoundManagerV1 : MonoBehaviour
{
    public static SoundManagerV1 Instance { private set; get; }
    private void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
            DontDestroyOnLoad(this);
        }
        else
            Destroy(this);
    }
    //Unity官方demo里也有写在OnEnable中的情况,有点怪,不知道为什么这么做
    // private void OnEnable()
    // {
    //     if (Instance == null)
    //         Instance = this;
    //     else if (Instance != this) //注意这里,和Awake不同
    //         Destroy(Instance);
    // }
    public void PlaySound(string soundPath)
    {
        Debug.LogFormat("播放音频:{1},内存地址是:{1}", soundPath, this.GetHashCode());
    }
}

この方式には次の問題があります。

  1. スクリプトは最初に最初のシーンにマウントする必要があります。そうでない場合は、最初のユーザーが AddComponent を使用してシングルトンを初期化する必要があります。

この動作自体は非常に珍しいものです。それは非常に厄介な空腹の中国のシングルトンであると言えます。

  1. Hungry Chinese シングルトンの一般的な問題: Awake でインスタンスにアクセスすると、null 参照のリスクがあります。

このリスクは、純粋な C# で実装された単純なシングルトンよりも深刻です。

その理由は、どのスクリプトが最初に Awake を呼び出すかは私たちには制御できないからです。

TheSingleton.Instance を呼び出すときに、TheSingleton の Awake がまだ実行されていないため、TheSingleton.Instance でヌル ポインター エラーが発生する可能性があります。

改良版

空腹から怠け者まで


using UnityEngine;
public class SoundManagerV2 : MonoBehaviour
{
    private static SoundManagerV2 _Instance = null;
    public static SoundManagerV2 Instance
    {
        get
        {
            if (_Instance == null)
            {
                _Instance = FindObjectOfType<SoundManagerV2>();
                if (_Instance == null)
                {
                    GameObject go = new GameObject();
                    go.name = "SoundManager";
                    _Instance = go.AddComponent<SoundManagerV2>() as SoundManagerV2;
                    DontDestroyOnLoad(go);
                    Debug.LogFormat("初次复制后,单例的地址:{0}", _Instance.GetHashCode());
                }
            }
            Debug.LogFormat("单例的地址:{0}", _Instance.GetHashCode());
            return _Instance;
        }
    }
    private void Awake()
    {
        if (_Instance == null)
            _Instance = this;
        else
            Destroy(this);
    }
    public void PlaySound()
    {
        Debug.LogFormat("v2播放音频,内存地址是:{0}", this.GetHashCode());
    }
}

このバージョンではすでに遅延初期化が実装されているため、null ポインタ エラーを心配する必要はありません。

同時に、GameObject を独自に生成することができ、最初の追加には AddComponent() を使用しなければならないという制限はなくなりました。

実装に関してはこれで十分ですが、唯一の問題は拡張です。次にシングルトン クラスを作成するときは、コードをコピーして貼り付ける (または手動でもう一度行う) 必要があります。

改善方法はもちろんジェネリッククラスを使って実装します!

ジェネリックを使用した改良版

ジェネリックは、複数の型がコードのセットを共有するためのより洗練された方法を提供します。

コードのこの部分はUnityCommunity/UnitySingletonからのものです


using UnityEngine;
public abstract class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
    #region 局部变量
    private static T _Instance;
    #endregion
    #region 属性
    /// <summary>
    /// 获取单例对象
    /// </summary>
    public static T Instance
    {
        get
        {
            if (null == _Instance)
            {
                _Instance = FindObjectOfType<T>();
                if (null == _Instance)
                {
                    GameObject go = new GameObject();
                    go.name = typeof(T).Name;
                    _Instance = go.AddComponent<T>();
                }
            }
            return _Instance;
        }
    }
    #endregion
    #region 方法
    protected virtual void Awake()
    {
        if (null == _Instance)
        {
            _Instance = this as T;
            DontDestroyOnLoad(gameObject);
        }
        else
        {
            Destroy(gameObject);
        }
    }
    #endregion
}

ソースコード

完全なコードは [nickpansh/Unity-Design-Pattern | GitHub]( https://github.com/nickpansh/Unity-Design-Pattern) にアップロードされているため、必要に応じて自分で使用できます。

エピローグ

私なら、純粋な C# で実装されたオプション 6 を使用するか、MonoBehaviour に基づく最後のオプションを使用します。

どの解決策を使用するかは、実際の状況によって異なります。

コードは機能的なサービス用であり、一部の最適化は当然のものとは見なされず、結果としてマイナスの最適化が行われます。

おすすめ

転載: blog.csdn.net/nick1992111/article/details/128608037