Одиночка (Singleton, Синглтон) – порождающий паттерн, который гарантирует, что для определенного класса будет создан только один объект, а также предоставит к этому объекту точку доступа.
Когда надо использовать Синглтон? Когда необходимо, чтобы для класса существовал только один экземпляр
Синглтон позволяет создать объект только при его необходимости. Если объект не нужен, то он не будет создан. В этом отличие синглтона от глобальных переменных.
Классическая реализация данного шаблона проектирования на C# выглядит следующим образом:
class Singleton
{
private static Singleton instance;
private Singleton()
{}
public static Singleton getInstance()
{
if (instance == null)
instance = new Singleton();
return instance;
}
}
В классе определяется статическая переменная – ссылка на конкретный экземпляр данного объекта и приватный конструктор. В статическом методе getInstance() этот конструктор вызывается для создания объекта, если, конечно, объект отсутствует и равен null.
Для применения паттерна Одиночка создадим небольшую программу. Например, на каждом компьютере можно одномоментно запустить только одну операционную систему. В этом плане операционная система будет реализоваться через паттерн синглтон:
class Program
{
static void Main(string[] args)
{
Computer comp = new Computer();
comp.Launch("Windows 8.1");
Console.WriteLine(comp.OS.Name);
// у нас не получится изменить ОС, так как объект уже создан
comp.OS = OS.getInstance("Windows 10");
Console.WriteLine(comp.OS.Name);
Console.ReadLine();
}
}
class Computer
{
public OS OS { get; set; }
public void Launch(string osName)
{
OS = OS.getInstance(osName);
}
}
class OS
{
private static OS instance;
public string Name { get; private set; }
protected OS(string name)
{
this.Name=name;
}
public static OS getInstance(string name)
{
if (instance == null)
instance = new OS(name);
return instance;
}
}
Реализация шаблона в общем виде
- объявляем только закрытый конструктор, чтобы запретить создание экземпляров извне;
- в закрытом поле размещаем единственный экземпляр класса;
- предоставляем доступ к нему через свойство, открытое только для чтения;
- клиентский код использует это свойство для получения общего экземпляра класса.
Примеры реализации
Рассмотрим один интересный момент: единственный экземпляр класса будет размещен в статической переменной. В .NET это обеспечит потокобезопасность его инициализации.
Однако, остается еще одна проблема: тип класса, содержащего статическую переменную, получит отметку BeforeFieldInit. При ее наличии, нет гарантий, в какой момент будут инициализированы статические поля класса (ECMA 335: спецификация CLI, раздел 8.9.5).
Таким образом, нельзя определить, когда будет создан экземпляр Одиночки. Но есть несколько подходов, гарантирующих, что это произойдет только при первом обращении:
- для размещения экземпляра Одиночки создается вложенный класс со статическим конструктором. Его наличие заставит компилятор не добавлять отметку BeforeFieldInit. В этом случае, согласно спецификации, инициализация будет при первом обращении.
- в .NET 4 можно использовать класс Lazy.
Рассмотрим реализации подробнее. При изучении кода, обратите внимание, что:
- код инициализации класса потокобезопасный;
- private-конструктор, по сути, запрещает создание наследников. При этом класс, для большей наглядности, можно отметить как закрытый (sealed);
- для поля _instance, хранящего экземпляр класса, используется ключевое слово readonly. Это защитит его от возможности случайного изменения в методах класса.
1. Реализация с вложенным классом
/// <summary>Thread-safe singleton.</summary>
public sealed class Singleton
{
/// <summary>Initializes a new instance of the <see cref="Singleton"/> class.</summary>
private Singleton() { }
/// <summary>Gets the singleton instance.</summary>
public static Singleton Instance { get { return InstanceHolder._instance; } }
/// <summary>Using nested class as instance storage.</summary>
protected class InstanceHolder
{
/// <summary> Explicit static constructor to tell C# compiler
/// not to mark type as beforefieldinit.</summary>
static InstanceHolder() { }
/// <summary>The one and only instance of the Singleton class.</summary>
internal static readonly Singleton _instance = new Singleton();
}
}
2. Реализация для .NET 4 с использованием класса Lazy
/// <summary>Thread-safe .NET4 lazy singleton.</summary>
public sealed class Singleton
{
/// <summary>The one and only instance of the Singleton class.</summary>
private static readonly Lazy<Singleton> _instance =
new Lazy<Singleton>(() => new Singleton());
/// <summary>Initializes a new instance of the <see cref="Singleton"/> class.
/// For internal use only.</summary>
private Singleton() { }
/// <summary>Gets the singleton instance.</summary>
public static Singleton Instance { get { return Singleton._instance.Value; } }
}
И в завершении, еще один вариант реализации. Это generic-класс, наследование от которого делает из его потомка Одиночку. За основу возьмем версию с классом Lazy. Для вызова private конструктора используем Reflection. Что лучше использовать: написать пару строк кода или наследование – решать вам.
3. Реализация generic-класса для шаблона Singleton
/// <summary>Thread-safe singleton.</summary>
public class Singleton<T> where T : class
{
/// <summary>The one and only instance of the Singleton class.</summary>
private static readonly Lazy<T> _instance = new Lazy<T>(
() => (T)typeof(T).GetConstructor(
BindingFlags.Instance | BindingFlags.NonPublic,
null, new Type[0], null).Invoke(null));
/// <summary>Gets the singleton instance.</summary>
public static T Instance { get { return Singleton<T>._instance.Value; } }
}
Важный момент: при использовании в своем классе необходимо объявить private конструктор, даже если он пустой.
public sealed class MySingleton : Singleton<MySingleton>
{
private MySingleton() { }
public void DemoMethod()
{
Console.WriteLine("MySingleton Test");
}
}
class Program
{
static void Main(string[] args)
{
MySingleton instance = MySingleton.Instance;
instance.DemoMethod();
}
}
Generic-версия с вложенным классом остается, как говорится, для самостоятельной работы.