More Singleton
Ok, so I lied: I'm not moving on to unions quite yet. Cyrus brought up a great point on the last post about .NET serialization, as well as the lack of thread safety in my third example. Due to the shared nature of singletons, I have to agree that thread-safety is an important enough requirement to mandate an expansion of the singleton story...
In addition, Andrew provided a great link about some other thoughts on implementing singletons in .NET.
http://www.yoda.arachsys.com/csharp/singleton.html
It introduces a neat way to resolve the thread safety by relying on the lazy loading of types in .NET. Although the article from arachsys.com doesn't mention serialization, if you read the MSDN reference on System.Runtime.Serialization.SerializationInfo, its example includes a way to control reinstantiation during deserialization in order to obtain the shared instance instead of the default serialization behavior of invoking a constructor. In doing so, you have to create a type that proxies for you during serialization -- but there's no reason why it shouldn't be the same nested type used for delay loading of the lazy-shared instance. Thus, whip in some explicit interface implementations (comment if you really want me to cover this topic), and voila!, you get the following:
|
[Serializable]
sealed class Foo : ISerializable {
// change the default constructor to private
private Foo() { }
public static Foo Only {
get { return SerializationProxy.sharedOnly; }
}
public object DoFunctionality();
[Serializable]
private class SerializationProxy : IObjectReference {
internal static readonly Foo sharedOnly = new Foo();
object IObjectReference.GetRealObject(StreamingContext context) {
// When deserializing this object, return a reference to
// Foo's singleton object instead.
return sharedOnly;
}
}
[SecurityPermission(SecurityAction.Demand,SerializationFormatter=true)]
void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context) {
info.SetType(typeof(Foo.SerializationProxy));
}
} |
The example differs from both the MSDN example and the arachsys.com article, but I'm pretty convinced it's the right thing to do. Creating a new type for serialization proxying is pretty much necessary unless you want to change the constructor semantics for the singleton type itself. Consider the following:
Assume type Foo is its own serialization proxy. During deserialization, the proxy instance of Foo is deserialized by default serialization means -- which involves invoking some constructor (either compiler provided default constructor, or user-coded). Thus you lose the invariant that any instance constructor is only invoked once, since some instance constructor has to be invoked at every proxy deserialization before GetRealObject() is invoked on the proxy.
It seems better to “de-singleton” the proxy type and hide it away, and a private nested type comes natural to my mind. Since it's nested, it allows us to also follow the arachsys.com and change the loading of the actual singleton instance to a form of "lazy loading", without ever having to introduce a synchronization keyword (e.g. "lock") or use the synchronization API. Albeit, the code doesn't really reflect in its structure that it's particularly both threadsafe and lazy-loading.
So, why do we really have to worry about serialization? Mostly because any of these implementations only guarantee one instance per AppDomain, which is somewhat like a process-like abstraction for .NET, except you can have more than one AppDomain per process. Each AppDomain has its own copies of static data though, so you can shield the effects on one against the other. Sometimes this is what you want to do (say you're running some untrusted code). However, this also prevents them from really sharing singleton instances. Also, when you have two .NET programs communicating across different processes (either concurrently, or different invocations using saved state), or on different computers, serialization becomes key.
I still probably use, in practice, the first or second simplistic singleton implementation -- with the addition of serialization support if necessary... but covering this topic has definitely brought new light to my own design toolbox -- I'll probably opt for this nested-lazy-serializable implementation a lot more in the future.