Паттерн Singleton в C#

58

Паттерн Singleton (Одиночка) является одним из наиболее известных шаблонов в разработке программного обеспечения. По сути Singleton – это класс, который позволяет создавать только один экземпляр и обычно предоставляет простой доступ к этому экземпляру.

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

В этой статье мы рассмотрим пример, когда параметры не требуются. Как правило, требование Singleton состоит в том, чтобы экземпляр создавался лениво (lazy) - т.е. экземпляр не создается до тех пор, пока он не понадобится.

Существуют различные способы реализации Singleton в C#. Я приведу некоторые из них здесь в обратном порядке элегантности, начиная с наиболее часто встречающихся. Все эти реализации имеют четыре общие характеристики:

При реализации Singleton мы будем использовать общедоступное статическое свойство с названием Source, как средство доступа к экземпляру. Во всех случаях свойство может быть легко преобразовано в метод, не влияя на потокобезопасность или производительность.

Первая версия - не потокобезопасная

// Пример реализации без использования потокобезопасности
public sealed class Singleton
{
    private Singleton()
    {
    }

    private static Singleton source = null;

    public static Singleton Source
    {
        get
        {
            if (source == null)
                source = new Singleton();

            return source;
        }
    }
}

Вышеуказанная реализация не является потокобезопасной. Два разных потока могли бы пройти условие if (source == null), создав два экземпляра, что нарушает принцип Singleton. Обратите внимание, что на самом деле экземпляр, возможно, уже был создан до того, как условие будет пройдено, но модель памяти не гарантирует, что новое значение экземпляра будет видно другим потокам, если не будут приняты соответствующие блокировки.

Вторая версия - простая защита от потоков

public sealed class Singleton
{
    private Singleton()
    {
    }

    private static Singleton source = null;
    private static readonly object threadlock = new object();

    public static Singleton Source
    {
        get
        {

            lock (threadlock)
            {
                if (source == null)
                    source = new Singleton();

                return source;
            }
        }
    }
}

Эта реализация является потокобезопасной. Поток создает блокировку для общего объекта threadlock, а затем проверяет, был ли экземпляр создан до создания текущего экземпляра. Это устраняет проблему с защитой памяти (поскольку блокировка гарантирует, что все чтения экземпляра класса Singleton будут логически происходить после завершения блокировки, а разблокировка гарантирует, что все записи будут выполняться логически до освобождения блокировки) и гарантирует, что только один поток создаст экземпляр. К сожалению производительность данной версии страдает, поскольку блокировка возникает всякий раз, когда запрашивается экземпляр.

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

Третья версия - потокобезопасная без использования lock

public sealed class Singleton
{
    // Явный статический конструктор сообщает компилятору C#
    // не помечать тип как beforefieldinit
    static Singleton() { }

    private Singleton() { }

    private static readonly Singleton source = new Singleton();

    public static Singleton Source
    {
        get
        {
            return source;
        }
    }
} 

Как вы можете заметить, это действительно очень простая реализация - но почему она является потокобезопасной и как в данном случае работает ленивая загрузка? Статические конструкторы в C# вызываются для выполнения только тогда, когда создается экземпляр класса или ссылается на статический член класса, и выполняются только один раз для AppDomain. Эта версия будет быстрее предыдущей, т.к. отсутствует дополнительная проверка на значение null. Однако в данной реализации есть несколько недочетов:

Четвертая версия - полностью ленивая загрузка

public sealed class Singleton
{
    private Singleton() { }

    public static Singleton Source { get { return Nested.source; } }

    private class Nested
    {
        static Nested()
        {
        }

        internal static readonly Singleton source = new Singleton();
    }
}

Здесь экземпляр инициируется первой ссылкой на статический член вложенного класса, который используется только в Source. Это означает, что эта реализация полностью поддерживает ленивое создание экземпляра, но при этом имеет все преимущества производительности предыдущих версий. Обратите внимание, что хотя вложенные классы имеют доступ к закрытым членам верхнего класса, обратное неверно, поэтому необходимо использовать модификатор internal. Это не вызывает никаких других проблем, поскольку сам вложенный класс является закрытым (private).

Пятый вариант - с использованием типа Lazy

Если вы используете версию .NET Framework 4 (или выше), вы можете использовать тип System.Lazy<T>, чтобы реализовать ленивую загрузку очень просто. Все, что вам нужно сделать, это передать делегат конструктору, который вызывает конструктор Singleton, которому передается лямбда-выражение:

public sealed class Singleton
{
    private Singleton() { }

    private static readonly Lazy<Singleton> lazy =
        new Lazy<Singleton>(() => new Singleton());

    public static Singleton Source { get { return lazy.Value; } }            
} 

Это довольная простая реализация, которая хорошо работает. Она также позволяет вам проверить, был ли экземпляр создан с использованием свойства IsValueCreated, если вам это нужно.

Пройди тесты
Лучший чат для C# программистов