Sui pattern ormai s'è detto e ridetto di tutto, tanto più su quelli famosi come il singleton. Nonostante ciò, mi capita spesso di incontrarne implementazioni opinabili. Butto giù due righe su quanto di meglio, per quel che so, si può fare nell'implementare il singleton.
Iniziamo dal classico problema: ho una windows form (diciamo class MyForm) che deve esistere solamente come singola istanza. Vorrei un metodo statico per crearla. Ossia mi piacerebbe poter scrivere: MyForm.Instance.Show(), per aprirla e ogni altra chiamata a MyForm.Instance dovrebbe ritornarmi un riferimento sempre alla medesima form.
La prima implementazione che un comune mortale osa immaginare è la classica descritta nel mitico "Design Patterns" del GoF:
public class MyForm : System.Windows.Forms.Form
{
protected static MyForm _instance = null;
public static MyForm Instance
{
get
{
if( _instance == null )
_instance = new MyForm();
return _instance;
}
}
protected MyForm() {}
}
Questa implementazione però pecca di parecchi problemi. Ad esempio: che accade se MyForm.Instance viene chiamata da due thread contemporaneamente e il sistema operativo esegue il context-switch subito dopo l'if ma prima del new? Banalmente verranno create sue istanze ma solo la seconda verrà riferita dal campo statico _instance. Se eredito la classe e richiamo nell'ordine Instance nella classe base e poi in quella derivata, come si deve comportare il mio tipo?
Queste domande (e relativi problemi) e un po' di acqua sotto i ponti, hanno portato qualche programmatore a costruire nuove implementazioni di singleton che fossero meno imbarazzate di fronte ad un ambiente multithreading. L'evoluzione della specie ha portato sino ai giorni nostri, questa particolare implementazione qui:
public sealed class MyForm : System.Windows.Forms.Form
{
private static volatile MyForm _instance = null;
public static MyForm Instance
{
get
{
if( _instance == null )
{
lock( typeof(MyForm) )
{
if( _instance == null )
_instance = new MyForm();
}
}
return _instance;
}
}
private MyForm() {}
}
Diamogli un occhio. Innanzitutto è stato implementato un controllo a doppio check per evitare la questione dei thread. Ma perchè proprio due if? E' abbastanza semplice. Supponiamo che due thread abbiano chiamato Instance. Il primo è in esecuzione, _instance è null e viene fermato dal sistema operativo subito dopo il primo if e prima del lock. A quel punto il secondo entra in azione supera il primo if ed entra nel monitor del lock, crea l'istanza ed esce dal monitor. Quando il primo thread riprenderà l'esecuzione si troverà a dover ripartire dopo aver già superato il primo if con successo, quindi entrerà nel monitor e verrà bloccato dal secondo if, in quanto l'istanza è già stata creata dal secondo thread.
Se evitassimo il secondo if, la ricreazione dell'istanza, in tale sfigatissimo caso, averrebbe.
Molto più sottile è la questione della parolina chiave volatile posta sul membro statico _instance. Per farla breve, serve ad evitare che eventuali ottimizzazioni del compilatore riordinino le istruzioni annullando il meccanismo dei doppi if.
Infine il sealed evita che la classe sia ereditabile. Se necessitate di permettere una ereditarietà vi consiglio di spulciare il "Design Patterns" per trovare un po' di ideuzze. In generale la soluzione non è banale e unica.
Dulcis in fundo, dopo tutto questo po' po' di ingegnosità è arrivato il .NET framework dove, miracolo dei miracoli, tutto può essere condensato in:
public sealed class MyForm : System.Windows.Forms.Form
{
public static readonly MyForm Instance = new MyForm();
private MyForm() {}
}
Può sembrare impossibile ma in una riga è nascosto quanto di meglio è stato detto sopra. Le inizializzazioni dei membri statici in .NET, infatti, sono sia lazy che thread-safe. Ossia avvengono al primo utilizzo del campo e il CLR garantisce la sincronia dei thread.
Voilà! ;)