Singleton (Одиночка)¶
Категория: порождающий паттерн.
Проблема¶
Иногда в приложении должен существовать ровно один экземпляр класса - конфигурация, реестр, логгер, кеш. Если позволить создавать сколько угодно экземпляров через new, можно получить рассинхронизацию состояния (например, два разных объекта конфигурации с разными значениями) или напрасный расход ресурсов (повторное открытие соединения, повторное чтение файла).
Решение¶
Singleton:
1. Прячет конструктор (делает его private), запрещая создание экземпляра снаружи.
-
Хранит единственный экземпляр в статическом поле.
-
Предоставляет статическое свойство/метод для доступа к этому экземпляру, создавая его лениво - при первом обращении.
Варианты реализации в .NET¶
Lazy<T>- самый идиоматичный способ.Lazy<T>уже умеет лениво и потокобезопасно инициализировать значение, поэтому нет смысла писать блокировки вручную.- Статический конструктор / инициализатор поля - CLR гарантирует, что статический конструктор типа выполнится не более одного раза и потокобезопасно, до первого обращения к любому статическому члену. Это самый простой вариант, если инициализация не требует параметров и происходит быстро.
- Double-checked locking - исторический вариант из книг про многопоточность, актуален, если нужен полный контроль над моментом и способом блокировки. В обычном прикладном коде почти всегда достаточно
Lazy<T>.
Когда применять¶
- Нужен ровно один экземпляр на всё приложение, и это ограничение - часть предметной области, а не просто "удобство".
- Экземпляр дорого создавать (соединение, кеш, пул ресурсов), и повторные создания нежелательны.
Когда НЕ стоит применять¶
- "Чтобы было удобно достать объект откуда угодно" - это признак не Singleton, а глобального состояния, которое усложняет тестирование (тесты начинают влиять друг на друга через общий статический объект) и скрывает зависимости класса (класс молча обращается к
X.Instanceвместо того, чтобы явно принять зависимость через конструктор). - Если единственная причина - "не хочу прокидывать зависимость через конструктор", это повод присмотреться к DI-контейнеру и
AddSingleton, а не к ручному Singleton.
Singleton vs Static Class¶
Статический класс (static class) в C# тоже даёт единственную "точку доступа", но:
- статический класс нельзя лениво инициализировать по частям и нельзя реализовать интерфейс;
- Singleton - это обычный объект (реализует интерфейсы, может быть передан как параметр, можно управлять моментом его создания), просто с ограничением на количество экземпляров.
Singleton vs DI-контейнер (Ambient Context)¶
Ручной Singleton по сути создаёт скрытую глобальную зависимость (Ambient Context): любой код может достать AppSettings.Instance, и по сигнатуре метода не видно, что он вообще что-то использует. Регистрация services.AddSingleton<IAppSettings, AppSettings>() и получение зависимости через конструктор решает ту же задачу (один экземпляр на всё приложение), но зависимость становится явной и легко подменяемой в тестах. В новом коде на ASP.NET Core предпочтителен именно этот вариант, ручной Singleton уместен там, где DI-контейнера нет (библиотеки, консольные утилиты, низкоуровневый код).
Пример в .NET Framework / BCL¶
HttpClientв паттернеIHttpClientFactoryфактически переиспользуется как синглтон-подобныйHttpMessageHandler.System.Windows.Application.Currentв WPF.MemoryCache.Default(в старомSystem.Runtime.Caching).