Your implementation does the trick. For what it's worth, I would consider your implementation the current "traditional way". It is thread-safe. The static constructor is guaranteed to run only once, so you won't accidentally end up with two instances if two threads try to grab the instance for the first time simultaneously.
Laziness
There is one more thing you might want to consider though. Because of the way the static constructor works, it is executed the first time the class is referenced in the code. This means that the instance in your singleton is created, even when you don't try to grab the instance, but another static variable perhaps.
To fix this, you might want to make the creation of the instance lazy, so that it really only fires when you need it. To do this, you can use the Lazy<T>
class.
Sealed
This is mostly a formality, but it's nice when trying to do it formally. The sealed
keyword means that that class cannot be inherited from. The private constructor already ensured that, but this makes it more explicit.
Readonly
As Jesse mentioned in the comments, it's a good idea to make the instance field (lazy or not) readonly
. This prevents you from accidentally mucking up your singleton instance from within the class.
public sealed class Person
{
private Person() { }
private static readonly Lazy<Person> lazyPersonInstance = new Lazy<Person>(() => new Person());
public static Person GetPersonInstance()
{
return lazyPersonInstance.Value;
}
}
Your method GetPersonInstance
can also be a getter-property:
public static Person Instance => lazyPersonInstance.Value;
Exceptions
IEatBagels already posted an answer about throwing exceptions. I'll elaborate a bit on how it would work in this example. We're looking at the scenario where instantiating the singleton instance throws an exception.
In your code, this exception would be thrown when the static constructor is ran. As IEatBagels points out, when this happens, the type is broken for the rest of the "program". This means that you have one shot at creating your instance.
In my example, the initiation of the instance does not happen during the execution of the static constructor. All we do during static initialization - static constructor and static fields act similarly IIRC - is creating the Lazy<T>
object with the factory method. This method is only executed when lazyPersonInstance.Value
is called.
However, Lazy<T>
caches exceptions. This means that if the factory method throws an exception, that exception will be rethrown on every subsequent call to lazyPersonInstance.Value
. Without re-executing the factory method. So in the end, this is the same problem as the static constructor problem. The Lazy<T>
docs have the following to say:
The Lazy stands in for an actual T that otherwise would have been initialized at some earlier point, usually during startup. A failure at that earlier point is usually fatal. If there is a potential for a recoverable failure, we recommend that you build the retry logic into the initialization routine (in this case, the factory method), just as you would if you weren't using lazy initialization.
MSDN docs
So if you really must throw exceptions in your constructor, be sure to handle them in the factory method.
Exceptions and thread-safety
There's one more workaround for the problem of exceptions in constructors. Lazy
has three thread-safety modes, defined in the enum System.Threading.LazyThreadSafetyMode
which can be passed to the constructor of Lazy<T>
.
None
. No thread safety.
ExecutionAndPublication
. This is the default. This ensures that the factory method is executed only once, and that the object on lazy.Value
is always the same across threads.
PublicationOnly
. This only ensures thread-safety on lazy.Value
. It can happen that the factory method is executed simultaneously by multiple threads, but the resulting instance is still the same. The others are discarded. According to the Lazy<T>
docs, Exceptions are not cached here.
This leaves you, the implementer of the singleton with a decision: if there are exceptions that might be thrown, that can't be handled within the factory method, and that might not throw in subsequent attempts (weird, but might happen), you could consider loosening up some of the locking on Lazy
to allow for this behaviour.
For further reading, see this blog post by Jon Skeet.
SingleTonSample
in the article OP posted. \$\endgroup\$