Best practice of Unity singleton mode (with code)


introduction

Systematically sort out several solutions for implementing singletons in Unity.

Implementation solutions are provided for two scenarios:

  1. Pure C# implementation (7 types)

  1. Inherited from MonoBehaviour (3 types)

The advantages and disadvantages of various solutions are analyzed, and the recommended elegant solution is given, that is, the best solution to realize the singleton mode in Unity.

In Unity, a class that inherits from MonoBehaviour implements a singleton differently than a singleton that does not inherit from MonoBehaviour implements a singleton.

Some people don't understand this point. They copied a part of C#'s implementation into the MonoBehaviour subclass, but it didn't work and they wrote nonsense.

For the difference between MonoBehaviour and pure C# on the constructor, please refer to my answer

As for whether you want to use the MonoBehaviour solution or the pure C# solution, it depends purely on your needs.

The pure C# implementation has better performance, but it cannot mount components, and it will be troublesome to debug.

Pure C# implements singleton mode (not inherited from Mono Behaviour)

grateful

This part of the content largely refers to the singleton implementation chapter in "C# In Depth" .

I just supplemented my understanding and added a generic implementation

text

"C# In Depth" provides 6 solutions to implement singletons in c#, sorted from the least elegant to the most elegant.

The sixth version is a crushing advantage, and Tukuai can directly jump to the sixth version.

These six programs have four common features:

  • There is only one private constructor with no parameters. This guarantees that other classes cannot initialize it (would break the singleton design pattern).

  • Inheritance also leads to broken design patterns. So it should be sealed. Although not required, it is recommended. May help JIT to optimize.

  • There is a static variable _Instance.

  • There is a public Instance method for obtaining singletons.

First version - not thread safe

This code is the simplest lazy style.

// 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;
        }
    }
}

It is not thread safe. (instance == null) The judgment in multiple threads is inaccurate, and each instance will be created, which violates the singleton pattern.

The second version - simple thread-safe writing

This program adds a lock.

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;
            }
        }
    }
}

The advantage is that the thread is safe, but the performance is affected-every time the get acquires the lock.

The third solution - use double check + lock to achieve thread safety

This solution has been widely recommended on the Internet, but the author directly labeled it as bad code and does not recommend it.

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;
        }
    }
}

This scheme optimizes scheme 2 by adding a ( instance == null ) judgment, which improves performance. But he has four shortcomings:

  • He doesn't work in Java.

  • There are no memory barriers anymore. It may be safe under the .NET 2.0 memory model, but I'd rather not rely on those stronger semantics. (is a question)

  • It's easy to get it wrong. The schema needs to be exactly the same as the code above - any breaking changes may impact performance or correctness. (If you use generics, you can avoid this problem. If you don’t use it, just copy it, the problem is big.)

  • Its performance is still not as good as the latter implementation. (is a question)

The fourth solution - Hungry Chinese style (no lazy initialization, but thread-safe without locks)

The previous three solutions are all lazy. They are inherently thread-unsafe. We use locks for thread safety. Using locks will bring additional overhead, which is inevitable. In fact, it is very good to use Hungry Chinese style directly.

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;
        }
    }
}

As you can see, the code is very simple.

A static constructor in C# will only be called when its class is used to create an instance or it has a static member referenced.

Obviously, this scheme is faster than the second and third schemes with additional checks added above. but:

  • It is not as "lazy" (i.e. delayed) as other solutions.

Especially if you have other static methods, then when you call other static methods, Instance will also be generated. (the next solution can solve this problem)

  • Complications arise if one static constructor calls another static constructor.

See the .NET spec for more details - this question probably won't "bite" you, but it's worth looking at the execution order of static constructors inside a loop.

  • There is a pitfall here. There is a difference between a static constructor and a type initializer. For details, please refer to: C# and beforefieldinit

The fifth solution - completely lazy style (completely lazy initialization)

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();
    }
}

This solution nests an inner class to implement it. In terms of effect, it is a little better than the fourth solution, but it is very unconventional.

The sixth solution - using the lazy type of .NET 4

If you're using .NET 4 or later, you can use System.Lazy to easily implement lazy initialization of objects. All you need is to pass (Delegate) to call the constructor, which can be easily done with Lambda expressions.

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属性来检查实例是否已经创建好。

### 关于性能与延迟初始化
在许多情况下,其实你并不需要完全的延迟初始化(即你不需要那么追求懒汉式)——除非你的类的构造非常耗时或者有一些别的副作用。
Quick Fact - .NET version used by Unity
  • Before Unity 2017.1, the .NET version used by Unity was .NET 3. This new syntax cannot be used

  • Unity 2017.1~Unity 2018.1, introduced .NET 4.1 as a trial version

  • Unity 2018.1~now .NET 4.x has become the standard configuration. (Compatible with .NET Standard 2.1)

You can view the currently used .NET version in Unity's Api Compatibility Level* list

Confirm the .NET version before using this method.

Conclusions from the author of "Depth in C#"

The solution recommended by the author of Depth in C# is solution 4, which is a simple hungry Chinese style.

He usually uses option 4, unless he needs to be able to call other static methods without triggering initialization.

Solution 5 is elegant, but more complicated than 2 or 4, and it provides too little benefit.

Solution 6 is the best solution, provided you are using .NET 4+.

Even so, the current tendency is to use option 4 - just follow the habit.

But if he is working with inexperienced developers, he will use option 6, as a simple and commonly used pattern.

Option 1 is rubbish.

Option 3 will not be used, and I would rather use Option 2 than Option 3.

He also criticized the cleverness of Solution 3, and the overhead of locks is not that big, and the way of writing in Solution 3 just seems to be optimized.

He has written test code to verify the overhead, and the difference between the two is very small.

It's a common misinformation that locks are expensive.

If you really think it is expensive, you can store the Instance outside the calling loop or simply use scheme 5.

In short, 4=6>5>2>3=1

My recommendation - which implementation to use in Unity

Unity development still has the characteristics of Unity development.

If you don't use multi-threading in Unity, the Mono code is originally single-threaded, and the official recommendation is to use coroutines instead of multi-threading.

And lazy initialization may not be a good thing for game development:

From the perspective of player experience, dropped frames are worse than slow startup/long Loading.

So option 3 is not advisable in most cases - you don't use multi-threading, but you pay extra for thread safety, and you keep saying that you have optimized it...

Option 6 is a rolling advantage

Of course, in the case of an older version of .NET, I think option 7 can be considered - option 3 implemented using generics.

Even so, Scheme 7 is worse than Scheme 6 and Scheme 4.

So my order of preference is:

Scheme 6[Lazy grammar implementation] > Scheme 4 (simple hungry style) > Scheme 7 (thread-safe lazy style implemented by generics) > 1 (do not consider thread safety if you do not write multi-threading) > 5>2>3

Specifically, it depends on your needs. There is no optimal implementation, only the most suitable one .

It may be controversial to rank so high for Option 1.

My core point is: if you don't use multithreading, you shouldn't add expensive optimizations for multithreading. This is negative optimization.

Don't believe it, you can take a look at how to implement a singleton in pure C# in the Unity source code.

How does the Unity source code realize the singleton

In fact, Unity provides such a singleton generic: [ScriptableSingleton](

https://docs.unity3d.com/cn/2021.3/ScriptReference/ScriptableSingleton_1.html) , it does not inherit from MonoBehaviour, it is implemented in pure C#

ScriptableSingleton allows you to create "Manager" types in the editor.

In classes derived from ScriptableSingleton, the serialized data you add will still take effect after the editor reloads the assembly.

If you use the FilePathAttribute in your class, the serialized data will persist across Unity Sessions.

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()
    {
        // 一系列初始化代码
    }
    //其他代码……
}

Ha, it's option 1.

Anyway, here is the seventh solution - the generic implementation of solution 3

The seventh scheme - the generic implementation of scheme 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;
        }
    }

}

After talking about the implementation of pure C#, let's take a look at the implementation of inheriting MonoBehaviour.

If you don't need to mount to the scene, then pure C# implementation is recommended.

If you need to hang the singleton in the scene, you can use the singleton inherited from MonoBehaviour.


Singleton inherited from MonoBehaviour

Some small problems inherited from the MonoBehaviour singleton

The main problem of inheriting from MonoBehaviour is that it cannot prevent the caller from adding a new singleton object through AddComponent.

This has violated the design principles of singletons to a certain extent.

In order to maintain the uniqueness of the singleton, we can only Destroy the newly generated Component. There is overhead here, which is not as good as pure C# implementation.

grateful

This part mainly refers to these two articles.

The most basic solution

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());
    }
}

This scheme has the following problems:

  1. The script must be mounted in the initial scene first, otherwise the first user has to initialize the singleton through AddComponent.

This behavior itself is very unusual. It can be said to be a very awkward hungry Chinese singleton.

  1. A common problem of Hungry Chinese singletons: If you access Instance in Awake, there is a risk of null references.

This risk is more serious than a simple singleton implemented in pure C#.

The reason is that which script calls Awake first is out of our control.

When calling TheSingleton.Instance, it is possible that TheSingleton.Instance encounters a null pointer error, because the Awake of TheSingleton has not been executed yet.

Improved version

From hungry to lazy


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());
    }
}

This version has already implemented lazy initialization, so there is no need to worry about null pointer errors.

At the same time, it can generate GameObject by itself, and there is no longer the restriction that AddComponent() must be used for the first addition.

This is already good enough in terms of implementation, the only problem is extension - next time we write a singleton class we need to copy paste the code (or manually re-write it).

The improvement method is of course implemented using generic classes!

Improved version using generics

Generics provide a more elegant way for multiple types to share a set of code.

This part of the code comes from 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
}

source code

The complete code has been uploaded to [nickpansh/Unity-Design-Pattern | GitHub]( https://github.com/nickpansh/Unity-Design-Pattern), you can use it yourself if you need it

epilogue

I would use option 6 implemented in pure C#, or the last option based on MonoBehaviour.

Which solution to use depends on the actual situation.

The code is for functional services, and some optimization cannot be taken for granted, and the result is negative optimization.

Guess you like

Origin blog.csdn.net/nick1992111/article/details/128608037