Design Pattern: Singleton - Class design vs Lazy<T> vs Container
Mohamed Ebrahim
System Architect | Software Designer | i help software users get better software by focusing on code quality and Domain-Driven Design with .NET. | Consultant | Lecturer
What problems does singleton solve?
A singleton is a class that has been designed to only ever allow one instance of itself to be created. The class itself is responsible for enforcing this design requirement. Single instances of classes are often required to model some kind of shared resource, like the file system or a shared network resource, like a scanner or print spooler
ways to achieve Singleton
Singleton Structure
the singleton is pretty simple. It's just a single class with
Singleton ?Features
Singleton implementation - Class Design
the first version - not thread-safe
public sealed class Singleton
??? {
??????? private static Singleton? _instance;
??????? public static Singleton Instance
??????? {
??????????? get
??????????? {
??????????????? Logger.Log("Instance called.");
??????????????? return _instance ??= new Singleton();
??????????? }
??????? }
??????? private Singleton()
??????? {
??????????? // cannot be created except within this class
??????????? Logger.Log("Constructor invoked.");
??????? }
??? }
the second version - simple thread-safe
public sealed class Singleton
??? {
??????? private static Singleton _instance = null;
??????? private static readonly object padlock = new object();
??????? public static Singleton Instance
??????? {
??????????? get
??????????? {
??????????????? Logger.Log("Instance called.");
??????????????? lock (padlock) // this lock is used on *every* reference to Singleton
??????????????? {
??????????????????? if (_instance == null)
??????????????????? {
??????????????????????? _instance = new Singleton();
??????????????????? }
??????????????????? return _instance;
??????????????? }
??????????? }
??????? }
??????? private Singleton()
??????? {
??????????? // cannot be created except within this class
??????????? Logger.Log("Constructor invoked.");
??????? }
??? }
The third version - attempted thread-safety using double-check locking
public sealed class Singleton
??? {
??????? private static Singleton _instance = null;
??????? private static readonly object padlock = new object();
??????? public static Singleton Instance
??????? {
??????????? get
??????????? {
??????????????? Logger.Log("Instance called.");
??????????????? if (_instance == null) // only get a lock if the instance is null
??????????????? {
??????????????????? lock (padlock)
??????????????????? {
??????????????????????? if (_instance == null)
??????????????????????? {
??????????????????????????? _instance = new Singleton();
??????????????????????? }
??????????????????? }
??????????????? }
??????????????? return _instance;
??????????? }
??????? }
??????? private Singleton()
??????? {
??????????? // cannot be created except within this class
??????????? Logger.Log("Constructor invoked.");
??????? }
??? }
Analysis
We added locking to enforce thread safety, ensuring only one thread at a time can enter the block in which we create the instance of the class. The first approach worked, but the lock applied to every GET request, even though it's only necessary when the instance hasn't yet been created, and that should only happen one time in the entire life of the application, so we're paying this price for the whole life of the application when we only need it once. The subsequent version is better since it uses double?checked locking. However, it also has some issues in that it's complex, it's easy to get wrong
Static Constructors and Singletons
Another approach that doesn't involve locking is to leverage the C# feature of static type construction. C# static constructors run once per app domain, so they are a good tool to consider when it comes to implementing singleton behavior. They are called the first time any static member of a type is referenced. This provides us with some degree of lazy instantiation, although it's not perfect since any reference to any static member, not necessarily our singleton reference, will trigger constructor execution. Make sure you use an explicit static constructor to avoid issues with the C# compiler and beforefieldinit. Essentially, beforefieldinit is a hint the compiler uses to let it know static initializers can be called sooner, and this is the default if the type does not have an explicit static constructor. Adding an explicit static constructor avoids having beforefieldinit applied, which helps make our singleton behavior lazier
the first version for the static constructor
if we read from any other static field or member of this class, we are going to also initialize our singleton instance
public sealed class Singleton
??? {
??????? private static readonly Singleton _instance = new Singleton();
??????? // reading this will initialize the _instance
??????? public static readonly string GREETING = "Hi!";
??????? // Tell C# compiler not to mark type as beforefieldinit
??????? // (https://csharpindepth.com/articles/BeforeFieldInit)
??????? static Singleton()
??????? {
??????? }
??????? public static Singleton Instance
??????? {
??????????? get
??????????? {
??????????????? Logger.Log("Instance called.");
??????????????? return _instance;
??????????? }
??????? }
??????? private Singleton()
??????? {
??????????? // cannot be created except within this class
??????????? Logger.Log("Constructor invoked.");
??????? }
??? }
So it's not as lazy as we would like. We can fix this, though, it just requires a little bit of tricky code.
The second version for the static constructor
In this case, we still have a read-only string GREETING, but our static instance now is going to return a nested class and its instance field. If we look here at the nested class, it's the nested class that has a read-only Singleton _instance that it's sharing up to the singleton type that we've wrapped around it. And this type, when it is requested, will only have its initializer called the first time that it's called. Note that there are no other static fields on the nested class, and that's how we get around the issue of accidentally loading it too soon if some other static member on this class is called. Note that we would like to make it so that the only thing that could access that Singleton _instance field is the wrapping class, but we can't mark it as private, so the closest we can get is marking it internal
public sealed class Singleton
??? {
??????? // reading this will initialize the instance
??????? public static readonly string GREETING = "Hi!";
??????? public static Singleton Instance
??????? {
??????????? get
??????????? {
??????????????? Logger.Log("Instance called.");
??????????????? return Nested._instance;
??????????? }
??????? }
??????? private class Nested
??????? {
??????????? // Tell C# compiler not to mark type as beforefieldinit (https://csharpindepth.com/articles/BeforeFieldInit)
??????????? static Nested()
??????????? {
??????????? }
??????????? internal static readonly Singleton _instance = new Singleton();
??????? }
??????? private Singleton()
??????? {
??????????? // cannot be created except within this class
??????????? Logger.Log("Constructor invoked.");
??????? }
??? }
Analysis
Let's review the static constructor approaches.
Lazy<T> and singleton
Lazy initialization of an object means that its creation is deferred until it is first used. (For this topic, the terms lazy initialization and lazy instantiation are synonymous.) Lazy initialization is primarily used to improve performance, avoid wasteful computation, and reduce program memory requirements
When you create a Lazy<T> type, you specify the type and a function that returns an instance of the type. We can use this to implement the singleton pattern
public sealed class Singleton
??? {
??????? // reading this will initialize the instance
??????? public static readonly Lazy<Singleton> _lazy = new Lazy<Singleton>(() => new Singleton());
??????? public static Singleton Instance
??????? {
??????????? get
??????????? {
??????????????? Logger.Log("Instance called.");
??????????????? return _lazy.Value;
??????????? }
??????? }
??????? private Singleton()
??????? {
??????????? // cannot be created except within this class
??????????? Logger.Log("Constructor invoked.");
??????? }
??? }
Singleton behavior using containers
Manage lifetime using a container, not class design
The below three methods define the lifetime of the services
Summary