Одиночка — это порождающий паттерн, который гарантирует существование только одного объекта определённого класса, а также позволяет достучаться до этого объекта из любого места программы.
Одиночка имеет такие же преимущества и недостатки, что и глобальные переменные. Его невероятно удобно использовать, но он нарушает модульность вашего кода.
Вы не сможете просто взять и использовать класс, зависящий от одиночки в другой программе. Для этого придётся эмулировать присутствие одиночки и там. Чаще всего эта проблема проявляется при написании юнит-тестов.
Применимость: Многие программисты считают Одиночку антипаттерном, поэтому его всё реже и реже можно встретить в 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#. Если интересно, можете ознакомиться с ними здесь: