
Одиночка на C#
Одиночка — это порождающий паттерн, который гарантирует существование только одного объекта определённого класса, а также позволяет достучаться до этого объекта из любого места программы.
Одиночка имеет такие же преимущества и недостатки, что и глобальные переменные. Его невероятно удобно использовать, но он нарушает модульность вашего кода.
Вы не сможете просто взять и использовать класс, зависящий от одиночки в другой программе. Для этого придётся эмулировать присутствие одиночки и там. Чаще всего эта проблема проявляется при написании юнит-тестов.
Сложность:
Популярность:
Применимость: Многие программисты считают Одиночку антипаттерном, поэтому его всё реже и реже можно встретить в C#-коде.
Признаки применения паттерна: Одиночку можно определить по статическому создающему методу, который возвращает один и тот же объект.
Наивный Одиночка (небезопасный в многопоточной среде)
Топорно реализовать Одиночку очень просто — достаточно скрыть конструктор и предоставить статический создающий метод.
Тот же класс ведёт себя неправильно в многопоточной среде. Несколько потоков могут одновременно вызвать метод получения Одиночки и создать сразу несколько экземпляров объекта.
Program.cs: Пример структуры паттерна
using System;
namespace RefactoringGuru.DesignPatterns.Singleton.Conceptual.NonThreadSafe
{
// Класс Одиночка предоставляет метод `GetInstance`, который ведёт себя как
// альтернативный конструктор и позволяет клиентам получать один и тот же
// экземпляр класса при каждом вызове.
// EN : The Singleton should always be a 'sealed' class to prevent class
// inheritance through external classes and also through nested classes.
public sealed class Singleton
{
// Конструктор Одиночки всегда должен быть скрытым, чтобы предотвратить
// создание объекта через оператор new.
private Singleton() { }
// Объект одиночки храниться в статичном поле класса. Существует
// несколько способов инициализировать это поле, и все они имеют разные
// достоинства и недостатки. В этом примере мы рассмотрим простейший из
// них, недостатком которого является полная неспособность правильно
// работать в многопоточной среде.
private static Singleton _instance;
// Это статический метод, управляющий доступом к экземпляру одиночки.
// При первом запуске, он создаёт экземпляр одиночки и помещает его в
// статическое поле. При последующих запусках, он возвращает клиенту
// объект, хранящийся в статическом поле.
public static Singleton GetInstance()
{
if (_instance == null)
{
_instance = new Singleton();
}
return _instance;
}
// Наконец, любой одиночка должен содержать некоторую бизнес-логику,
// которая может быть выполнена на его экземпляре.
public void someBusinessLogic()
{
// ...
}
}
class Program
{
static void Main(string[] args)
{
// Клиентский код.
Singleton s1 = Singleton.GetInstance();
Singleton s2 = Singleton.GetInstance();
if (s1 == s2)
{
Console.WriteLine("Singleton works, both variables contain the same instance.");
}
else
{
Console.WriteLine("Singleton failed, variables contain different instances.");
}
}
}
}
Output.txt: Результат выполнения
Singleton works, both variables contain the same instance.
Многопоточный Одиночка
Чтобы исправить проблему, требуется синхронизировать потоки при создании объекта-Одиночки.
Program.cs: Пример структуры паттерна
using System;
using System.Threading;
namespace Singleton
{
// Эта реализация Одиночки называется "блокировка с двойной проверкой"
// (double check lock). Она безопасна в многопоточной среде, а также
// позволяет отложенную инициализацию объекта Одиночки.
class Singleton
{
private Singleton() { }
private static Singleton _instance;
// У нас теперь есть объект-блокировка для синхронизации потоков во
// время первого доступа к Одиночке.
private static readonly object _lock = new object();
public static Singleton GetInstance(string value)
{
// Это условие нужно для того, чтобы не стопорить потоки блокировкой
// после того как объект-одиночка уже создан.
if (_instance == null)
{
// Теперь представьте, что программа была только-только
// запущена. Объекта-одиночки ещё никто не создавал, поэтому
// несколько потоков вполне могли одновременно пройти через
// предыдущее условие и достигнуть блокировки. Самый быстрый
// поток поставит блокировку и двинется внутрь секции, пока
// другие будут здесь его ожидать.
lock (_lock)
{
// Первый поток достигает этого условия и проходит внутрь,
// создавая объект-одиночку. Как только этот поток покинет
// секцию и освободит блокировку, следующий поток может
// снова установить блокировку и зайти внутрь. Однако теперь
// экземпляр одиночки уже будет создан и поток не сможет
// пройти через это условие, а значит новый объект не будет
// создан.
if (_instance == null)
{
_instance = new Singleton();
_instance.Value = value;
}
}
}
return _instance;
}
// Мы используем это поле, чтобы доказать, что наш Одиночка
// действительно работает.
public string Value { get; set; }
}
class Program
{
static void Main(string[] args)
{
// Клиентский код.
Console.WriteLine(
"{0}\n{1}\n\n{2}\n",
"If you see the same value, then singleton was reused (yay!)",
"If you see different values, then 2 singletons were created (booo!!)",
"RESULT:"
);
Thread process1 = new Thread(() =>
{
TestSingleton("FOO");
});
Thread process2 = new Thread(() =>
{
TestSingleton("BAR");
});
process1.Start();
process2.Start();
process1.Join();
process2.Join();
}
public static void TestSingleton(string value)
{
Singleton singleton = Singleton.GetInstance(value);
Console.WriteLine(singleton.Value);
}
}
}
Output.txt: Результат выполнения
FOO
FOO
Хотите ещё?
Существует ещё с полдюжины способов реализации Одиночки в C#. Если интересно, можете ознакомиться с ними здесь: