Перейти к содержанию

Singleton (Одиночка)

Категория: порождающий паттерн.

Проблема

Иногда в приложении должен существовать ровно один экземпляр класса - конфигурация, реестр, логгер, кеш. Если позволить создавать сколько угодно экземпляров через new, можно получить рассинхронизацию состояния (например, два разных объекта конфигурации с разными значениями) или напрасный расход ресурсов (повторное открытие соединения, повторное чтение файла).

Решение

Singleton: 1. Прячет конструктор (делает его private), запрещая создание экземпляра снаружи.

  1. Хранит единственный экземпляр в статическом поле.

  2. Предоставляет статическое свойство/метод для доступа к этому экземпляру, создавая его лениво - при первом обращении.

Варианты реализации в .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).

Пример реализации на C#

Singleton.cs
using System;
using System.Threading;

namespace DesignPatterns.Creational.Singleton
{
    /// <summary>
    /// Классический потокобезопасный Singleton на основе Lazy&lt;T&gt;.
    /// Lazy&lt;T&gt; сам по себе уже реализует ленивую и потокобезопасную инициализацию,
    /// поэтому вручную писать double-checked locking в 99% случаев не требуется.
    /// </summary>
    public sealed class AppSettings
    {
        private static readonly Lazy<AppSettings> LazyInstance =
            new(() => new AppSettings(), LazyThreadSafetyMode.ExecutionAndPublication);

        public static AppSettings Instance => LazyInstance.Value;

        public string ConnectionString { get; }

        // Приватный конструктор - никто, кроме самого класса, не может создать экземпляр через new.
        private AppSettings()
        {
            // Здесь могла бы быть тяжёлая инициализация: чтение файла конфигурации,
            // разбор переменных окружения и т.д. Она выполнится один раз, при первом обращении.
            ConnectionString = "Host=localhost;Database=demo";
        }
    }

    /// <summary>
    /// То же самое, но через статический конструктор - альтернативный вариант,
    /// который CLR гарантированно выполняет один раз и потокобезопасно ещё до первого
    /// обращения к любому статическому члену типа.
    /// </summary>
    public sealed class FeatureFlags
    {
        // Статический конструктор выполняется лениво (при первом обращении к типу)
        // и потокобезопасно - это гарантирует сам CLR.
        private static readonly FeatureFlags InstanceField = new();

        public static FeatureFlags Instance => InstanceField;

        public bool NewCheckoutEnabled { get; }

        private FeatureFlags()
        {
            NewCheckoutEnabled = true;
        }
    }

    public static class Demo
    {
        public static void Run()
        {
            var s1 = AppSettings.Instance;
            var s2 = AppSettings.Instance;

            Console.WriteLine(ReferenceEquals(s1, s2)); // True - это один и тот же объект
            Console.WriteLine(s1.ConnectionString);

            Console.WriteLine(FeatureFlags.Instance.NewCheckoutEnabled);
        }
    }
}

Открыть Singleton.cs отдельно Скачать Singleton.cs