SoFunction
Updated on 2025-03-07

Analyzing the singleton mode of C# design pattern

Singleton, so the name is that in the entire application, there should only be one instance of a certain object. For example, a class loads data in a database into memory to provide read-only data, which is very suitable for using singleton mode, because there is no need to load multiple copies of the same data in memory. In addition, in some cases, multiple copies of the same data are not allowed in memory, such as excessive data, no two copies of the same data are included in the memory, etc.

Singleton by Convention

This method is a bit "Too simple, sometimes naïve". It reminds the user that I am a singleton, don't repeatedly initialize me, such as:

public class Database
{
  /// <summary>
  /// Warning, this is a singleton, do not initialize it multiple times, otherwise you will be responsible for the consequences.  /// </summary>
  public Database() {}
};

One situation is that you won't notice this prompt at all. Secondly, in many cases, these initializations happen secretly and unintentionally, such as through reflection, production () through factory, injection, etc. Although there is a "convention greater than configuration", it is not used here.

The most common idea of ​​singleton pattern is to provide a global, static object.

public static class Globals
{
  public static Database Database = new Database();
}

This method is not very safe. This does not prevent the user from new Database elsewhere, and the user may not know that there is a Globals class with a Database singleton.

Classic implementation

The only way to prevent users from instantiating objects is to turn the constructor into private and provide methods or attributes to return unique internal objects.

public class Database
{
  private Database() { ... }
  public static Database Instance { get; } = new Database();
}

Now set the constructor to private. Of course, it can still be called through reflection, but this requires additional operations after all, which can prevent most users from instantiating directly. By defining an instance as static, its life cycle is extended until the application runs.

Delay initialization

The above methods are thread-safe, but because they are static properties, they will be initialized before all instances of the class are created or before any static members are accessed, and will only be initialized once in each AppDomain.

How to implement delayed initialization, that is, deferring the construction of a singleton object until the application first requests the object. If the application never requests the object, the object will never be constructed. Before, you could use the double check method. In fact, there are still some problems that need to be paid attention to in order to implement the correct double check. For example, in the above example, the first version may be written like this, using a lock.

public class Database
{
  private static Database db;
  private static object olock = new object();
  private Database()
  { }

  public static Database GetInstance()
  {
    lock (olock)
    {
      if (db == null)
      {
        db = new Database();
      }
      return db;
    }
  }
}

Although this is thread-safe, every time you access GetInstance, regardless of whether the object has been created, you need to obtain and release the lock, which consumes more resources. Therefore, add another layer of judgment outside.

public class Database
{
  private static Database db;
  private static object olock = new object();
  private Database()
  { }

  public static Database GetInstance()
  {
    if (db == null)
    {
      lock (olock)
      {
        if (db == null)
        {
          db = new Database();
        }
      }
    }
    return db;
  }
}

Determine whether it has been initialized before accessing the object. If the initialization is returned directly, this will avoid access to the lock. But, there are still problems here. Assuming that Database is initialized in time, when thread A obtains the lock and is initializing db, thread B determines whether db is empty at the outermost layer. At this time, thread A is initializing db, and it is possible that only part of it is initialized. At this time, db may not be empty and directly return an object that has not been completely initialized, which may cause thread B to crash.

The solution is to store the object into a temporary variable and then store it in db in atomic writing, as follows

public class Database
{
  private static Database db;
  private static object olock = new object();
  private Database()
  { }

  public static Database GetInstance()
  {
    if (db == null)
    {
      lock (olock)
      {
        if (db == null)
        {
          var temp = new Database();
          (ref db, temp);
        }
      }
    }
    return db;
  }
}

It is very cumbersome. Although delayed initialization is implemented, it is too complicated compared to the static field at the beginning, and it will be written incorrectly if you are not careful. Although it can be simplified to:

public static Database GetInstance()
{
  if (db == null)
  {
    var temp = new Database();
    (ref db, temp, null);
  }
  return db;
}

This method does not seem to be locked, and the temp object may be initialized twice, but when writing temp to db, it is guaranteed that only 1 object will be correctly written to db, and the temp object that has not been written will be garbage collected. This method is faster than the above double check. But learning costs are still required. Fortunately, C# provides the Lazy method:

private static Lazy<Database> db = new Lazy<Database>(() => new Database(), true);
public static Database GetInstance() => ;

Simple and perfect.

Dependency injection and singleton pattern

The previous singleton pattern is actually a code intrusion method. It is to implement a singleton that does not originally implement a singleton to implement a singleton, and the code needs to be modified to implement it, and this code is prone to errors. Some people think that the only correct way to do the singleton pattern is to use IOC dependency injection, so that there is no need to modify the source code. Dependency injection framework is implemented through the dependency injection framework. In a unified entrance, a unified management life cycle, in Core MVC, in Startup's ConfigureServices code:

<Database>();

Or add places where IDatabase is needed and Database singleton is used:

<IDatabase,Database>();

In the subsequent code of Core MVC, as long as IDatabase is used, Database singleton will be implemented, and we do not need to make any modifications in Database. When using it, you only need to refer to the GetService method in the IServiceProvider interface. IServiceProvider is directly provided by the Core MVC IOC framework and does not require special processing:

public XXXController(IServiceProvider serviceProvider)
{
   var db = <IDatabase>();
}

Monostate

Singleton mode is a variant of singleton mode. It is a normal class, but its behavior and performance are like singleton mode.

For example, when we are modeling the personnel structure of a company, a typical company generally only has one CEO.

public class ChiefExecutiveOfficer
{
  private static string name;
  private static int age;

  public string Name
  {
    get => name;
    set => name = value;
  }

  public int Age
  {
    get => age;
    set => age = value;
  }
}

The properties of this class include get and set, but the private fields behind it are all static. So no matter how many times the ChiefExecutiveOfficer is instantiated, the internal references are the same data. For example, two objects can be instantiated, but the contents inside them are exactly the same.

Single-state mode is simple, but it is easy to cause confusion. Therefore, if you want to be simple and to achieve singleton effect, it is best to use IOC, a dependency injection framework, to let it help us manage instances and their life cycles.

The above is the detailed content of analyzing the singleton pattern of C# design pattern. For more information about the singleton pattern of C#, please pay attention to my other related articles!