Как отложить статическую инициализацию в свойстве

Я создал класс, который представляет собой нечто среднее между синглтоном (пятая версия) и фабрика (вводимая зависимость). Назовите это «монофабрикой»? Это работает и выглядит так:

public static class Context
{
    public static BaseLogger LogObject = null;

    public static BaseLogger Log
    {
        get
        {
            return LogFactory.instance;
        }
    }

    class LogFactory
    {
        static LogFactory() { }
        internal static readonly BaseLogger instance = LogObject ?? new BaseLogger(null, null, null);
    }
}

//USAGE EXAMPLE:
//Optional initialization, done once when the application launches...
Context.LogObject = new ConLogger();

//Example invocation used throughout the rest of code...
Context.Log.Write("hello", LogSeverity.Information);

Идея состоит в том, чтобы монофабрику можно было расширить для обработки более одного элемента (например, более одного регистратора). Но мне бы хотелось, чтобы монофабрика выглядела так:

public static class Context
{
    private static BaseLogger LogObject = null;

    public static BaseLogger Log
    {
        get
        {
            return LogFactory.instance;
        }
        set
        {
            LogObject = value;
        }
    }

    class LogFactory
    {
        static LogFactory() { }
        internal static readonly BaseLogger instance = LogObject ?? new BaseLogger(null, null, null);
    }
}

Вышеупомянутое не работает, потому что в момент касания свойства Log (при вызове установщика) он вызывает выполнение пути кода, связанного с получателем ... что означает, что внутренние данные «экземпляра» LogFactory всегда устанавливаются в BaseLogger (устанавливать «LogObject» всегда слишком поздно!).

Итак, есть ли украшение или другой трюк, который я мог бы использовать, который заставил бы путь "get" свойства Log быть ленивым, пока вызывается установленный путь?


person Brent Arias    schedule 17.06.2010    source источник


Ответы (2)


Примечание. Это полностью переписанный исходный ответ; Однако рекомендация остается в силе.

Во-первых: убедитесь, что вы не работаете под отладчиком. Например, окно просмотра может касаться ваших общедоступных статических свойств. Это одна из возможных причин, по которой второй пример может вести себя иначе, чем первый. Это может звучать глупо, но никогда не угадаешь.

Под .NET 4 ваш второй пример действительно работает, и я честно ожидаю, что он будет работать и под .NET 2. Если вы случайно не коснетесь свойства Context.Log или поля LogFactory.instance. Тем не менее, он выглядит ужасно хрупким.

Кроме того, строго говоря, beforefieldinit тонкости, которые вы пытаетесь использовать здесь, могут укусить вас в многопоточном приложении: инициализацию LogFactory не нужно запускать в том же потоке, что и установщик Context.Log[Object]. Это означает, что при инициализации LogFactory.instance в этот поток Context.LogObject еще не нужно устанавливать, пока он находится в другом (такая синхронизация может происходить лениво). Так что он не потокобезопасен. Вы можете попытаться исправить это, сделав Context.LogObject изменчивым, чтобы набор был виден во всех потоках сразу. Но кто знает, в какие еще гоночные условия мы попадем дальше.

И после всех уловок вы все равно получаете следующий довольно неинтуитивный результат:

Context.Log = value1; // OK
Context.Log = value2; // IGNORED

Вы ожидаете, что второй вызов установщика либо сработает (Context.Log == value2), либо выбросит. Не следует молча игнорировать.

Вы также можете пойти на

public static class Context
{
    private static BaseLogger LogObject;

    public static BaseLogger Log
    {
        get { return LogObject ?? LogFactory.instance; }
        set { LogObject = value; }
    }

    private class LogFactory
    {
        static LogFactory() {}
        internal static readonly BaseLogger instance 
               = new BaseLogger(null, null, null);
    }
}

Здесь результат является гарантированным и ленивым (в соответствии с пятым одноэлементным методом Джона Скита). И ИМХО выглядит намного чище.

person Ruben    schedule 17.06.2010
comment
@Ruben: Я удалил свой ответ, потому что считаю, что он вводит в заблуждение и / или просто неправильно. +1 за ваше последнее предложение, которое делает гораздо более очевидным, что на самом деле делает код. - person LukeH; 18.06.2010
comment
@Ruben: Если среда CLR может произвольно выбрать последовательность инициализации, как вы предлагаете, означает ли это, что пятая версия синглтона Джона Скита не работает (yoda.arachsys.com/csharp/singleton.html)? В любом случае я, вероятно, изменю свой код, возможно, чтобы выбросить исключение после первого набора, или я воспользуюсь тем, что вы предложили. - person Brent Arias; 18.06.2010
comment
Нет, я думаю, что Джон Скит прав. Но обратите внимание, что в его коде нет зависимости от какого-либо порядка, и вложенный класс в его примере не использует никакой статики родительского класса. Я думаю, что мне нужно несколько скорректировать свой ответ, потому что я не учел некоторые неприятности beforefieldinit. - person Ruben; 18.06.2010
comment
Да, в коде Skeet # 5 это явно не показано, но гарантированный ленивый порядок инициализации - это, скорее, вся суть # 5 - именно для того, чтобы вы могли изменять состояние до или влиять на создание экземпляра (вот почему это единственный полностью ленивый реализация). Фактически, если бы это было не так, не было бы смысла иметь №5 (№4 всегда было бы достаточно). - person Brent Arias; 18.06.2010
comment
Я не уверен, что согласен с этим. Пункт №5 - полная ленивая инициализация. Больше ничего. Но в вашем коде есть возможное состояние гонки (см. Мой измененный ответ), которое не имеет отношения к цели Джона. - person Ruben; 18.06.2010

Несколько советов:

Ознакомьтесь с Generics

Я избегаю инициализации с использованием статического. На практике это может вызвать странные проблемы. Например, если то, что вы создаете, вызывает ошибку, загрузчик Windows сообщит вам о проблеме, но не скажет, что именно. Ваш код на самом деле никогда не вызывается, поэтому у вас нет возможности исключить проблему. Я конструирую первый экземпляр, когда он используется в первый раз. Вот пример:

    private static OrderCompletion instance;

    /// <summary>
    /// Get the single instance of the object
    /// </summary>
    public static OrderCompletion Instance
    {
        get
        {
            lock (typeof(OrderCompletion))
            {
                if (instance == null)
                    instance = new OrderCompletion();
            }
            return instance;
        }
    }
person Jay    schedule 17.06.2010
comment
Кажется довольно расточительным блокировать каждый доступ, по крайней мере, вы можете поместить вторую копию if (instance == null) вне оператора блокировки, чтобы избежать блокировки каждого вызова. - person Chris Marisic; 18.06.2010
comment
Я думаю, что это создаст окно возможностей для отказа в многопоточной среде. - person Jay; 18.06.2010