Есть ли веские причины не использовать оператор объединения с нулевым значением для ленивой инициализации?

Приветствую, сегодня я делал ленивый код инициализации и подумал, почему бы не использовать для этого оператор объединения с нулевым значением, он короче, но потом я подумал, есть ли какие-либо накладные расходы или дополнительные затраты на выполнение этого таким образом.

Ниже приведен упрощенный пример кода, показывающий более распространенную форму, используемую для отложенной инициализации, а затем форму с использованием оператора объединения с нулевым значением. Они имеют точно такие же результаты и кажутся эквивалентными. Мои первые мысли заключаются в том, что после того, как объект был создан, теперь происходит его дополнительное присвоение самому себе с помощью ??. Является ли это не проблемой, и компилятор/JIT каким-то образом оптимизирует это, происходит ли что-то более гнусное, и вам никогда не следует выполнять ленивую инициализацию с помощью ??, или это совершенно безопасно, и от него не может исходить плохое моджо.

private MyLazyObject _lazyObject;

public MyLazyObject GetMyLazyObjectUsingMoreCommonMethod()
{
    if (_lazyObject != null)
        return _lazyObject;

    _lazyObject = new MyLazyObject();

    return _lazyObject;
}

public MyLazyObject GetMyLazyObjectUsingNullCoalescingOpMethod()
{
    _lazyObject = _lazyObject ?? new MyLazyObject();
    return _lazyObject;
}

person Rodney S. Foley    schedule 13.09.2011    source источник
comment
Вы могли бы еще сократить его: return _lazyObject ?? (_lazyObject = new MyLazyObject()); Это все еще читабельно? YMMV.   -  person Marc    schedule 14.09.2011
comment
Если вы перевернете проверку в своем первом методе и сделаете if(_lazyObj == null) _lazyObj = new MyLazyObj(); return _lazyObj;, она будет более читабельной и короче.   -  person Adam Lear    schedule 14.09.2011
comment
Разве это не было бы просто: return _lazyObject ?? new MyLazyObject());   -  person Brad Cunningham    schedule 14.09.2011
comment
@Foo Ему нужно присвоить переменную, чтобы сохранить новый экземпляр.   -  person Adam Lear    schedule 14.09.2011
comment
@marc да, я так делаю :p   -  person Marc Gravell    schedule 14.09.2011
comment
Точно, я пропустил эту часть. Спасибо   -  person Brad Cunningham    schedule 14.09.2011
comment
@Энн, да, это более чистый способ сделать первый метод, который у меня был в моем примере, спасибо.   -  person Rodney S. Foley    schedule 14.09.2011
comment
C#8.0 позволяет еще больше сократить: return _lazyObject ??= new MyLazyObject();   -  person Zodman    schedule 04.02.2021


Ответы (4)


Да, маленькая вещь, называемая безопасностью потоков. Два указанных вами метода функционально эквивалентны, поэтому оператор объединения null сам по себе неплох, но ни один из перечисленных вами подходов не является потокобезопасным, поэтому, если два потока попытаются вызвать ваш метод Get одновременно , вы можете получить два MyLazyObject. Это может быть не так уж важно, но, вероятно, это не то, на что вы надеетесь.

Если вы используете .NET 4, просто используйте файл Lazy.

private Lazy<MyLazyObject> _lazyObject = 
    new Lazy<MyLazyObject>(() => new MyLazyObject());

public MyLazyObject MyLazyObject {get {return _lazyObject.Value;}}

Код краткий, простой для понимания и потокобезопасный.

person StriplingWarrior    schedule 13.09.2011
comment
@Foovanadil: Вот в чем проблема: ни один из них не является потокобезопасным. Потокобезопасность — веская причина не использовать ни тот, ни другой подход. - person StriplingWarrior; 14.09.2011
comment
@Foovanadil: Чтобы уточнить, Lazy‹T› является потокобезопасным. См. msdn.microsoft.com/en-us/library/dd642259.aspx - person TrueWill; 14.09.2011
comment
Примечание: если конструктор вашего класса может генерировать исключение, вы, вероятно, не хотите использовать конструктор Lazy‹T› без аргументов. Это вызовет исключение TargetInvocationException. Вместо этого используйте конструктор Lazy‹T›, который принимает делегат Func‹T› (лямбда), как в примере @StriplingWarrior. Это кеширует и повторно выдает ваше исключение. - person TrueWill; 14.09.2011
comment
Спасибо, @StriplingWarrior, каждый день узнаю что-то новое о .Net 4, мне нравится курс Lazy‹T›. - person Rodney S. Foley; 14.09.2011
comment
Я немного не согласен здесь; потокобезопасность — это очень специфическая тема, и подавляющее большинство классов не обязательно должны быть потокобезопасными (и действительно: если бы они были, это немного более тонко, чем для каждого члена). Если потокобезопасность явно не заявлена ​​как требование, вводить ее искусственно (и класс вряд ли будет работать слаженно как потокобезопасная единица, если он специально не предназначен для этого). Lazy<T> добавляет небольшие накладные расходы, которые в большинстве случаев не нужны. - person Marc Gravell; 14.09.2011
comment
@MarcGravell на самом деле хорошая мысль, и я согласен. В моем случае я планирую запустить несколько потоков в этом проекте, и это повлияет на область, над которой я сейчас работаю. Вот почему мне понравился ответ StriplingWarrior, поскольку он был очень применим к моей ситуации. Однако я специально не упомянул безопасность потоков в своем вопросе, и у меня есть несколько правильных ответов, и я хотел бы проверить более одного ответа как правильный. Поскольку этот ответ дал мне решение, я в конечном итоге основывал свою реализацию на том, что я выбираю. Будем надеяться, что будущие читатели должны выбрать ответ, который лучше всего подходит для этой ситуации. - person Rodney S. Foley; 14.09.2011
comment
@Rodney, если многопоточность является проблемой, то это действительно очень подходящий ответ - person Marc Gravell; 14.09.2011
comment
@Marc Gravell: +1 Хорошо аргументировано. В зависимости от того, как предполагается использовать класс, Lazy может оказаться не лучшим решением. По моему личному опыту, объекты с ленивым созданием экземпляров часто предназначены для использования несколькими потоками, но это не так для всех. Просто когда кто-то спрашивает, это совершенно безопасно? Я думаю, стоит представить возможность, даже если это не было явным требованием. - person StriplingWarrior; 14.09.2011
comment
Кроме того, вы можете использовать LazyThreadSafetyMode.None, чтобы отключить накладные расходы на безопасность потоков в Lazy‹T› и при этом иметь код, который легко понять намерения и который может включить безопасность потоков, если потребуется позже. - person Rodney S. Foley; 14.09.2011
comment
Я понимаю, что Lazy‹T› является потокобезопасным. Я хотел сказать, что его вопрос не указывает на то, что многопоточность была проблемой или что многопоточность была проблемой, которую он пытался решить. Ответ с безопасностью потоков кажется мне неправильным. Ни один из его подходов или решений вообще не меняет поведение многопоточности. Я согласен с Марком здесь. Threading — это тема, отличная от того, о чем спрашивает пользователь. - person Brad Cunningham; 14.09.2011
comment
Итак, если мой класс не связан с потокобезопасностью, лучше ли мне избегать Lazy<> и связанных с ним накладных расходов, применяя технику проверки на ноль? - person Ayxan Haqverdili; 11.02.2021
comment
Да, наверное. - person StriplingWarrior; 13.02.2021

Это совершенно безопасно и хорошо определено - и действительно, это означает, что компилятор может просто скопировать голову стека (dup) и сохранить один раз, а не сохранить поле, загрузить поле.

Единственный раз, когда это проблема, - это С# 1.2 (.NET 1.1), где ее не существует.

person Marc Gravell    schedule 13.09.2011
comment
Что вы думаете о проблемах безопасности потоков, поднятых StriplingWarrior, CodeCaster Мэтью Эбботом? - person Rodney S. Foley; 14.09.2011

Оператор объединения null в синтаксическом сахаре. По сути, это то же самое, что и ваш первый пример, и я не верю, что JIT-компилятор делает для него специальную оптимизацию. Что вас должно больше беспокоить, так это потокобезопасность ваших методов. Оператор объединения null не является атомарным, а это означает, что вы должны убедиться, что ваш MyLazyObject создан потокобезопасным способом перед возвратом.

person Matthew Abbott    schedule 13.09.2011

Вы даже можете изменить код первого метода:

public MyLazyObject GetMyLazyObjectUsingMoreCommonMethod()
{
    if (_lazyObject == null)
        _lazyObject = new MyLazyObject();

    return _lazyObject;
}

Это доставит тот же IL, что и

public MyLazyObject GetMyLazyObjectUsingNullCoalescingOpMethod()
{
    _lazyObject = _lazyObject ?? new MyLazyObject();

    return _lazyObject;
}

Как уже было сказано, это просто синтаксический сахар.

person CodeCaster    schedule 13.09.2011