Любопытный C#, использующий расширение инструкции

Я запустил ildasm и обнаружил, что это:

    using(Simple simp = new Simple())
    {
        Console.WriteLine("here");
    }

генерирует код IL, который эквивалентен этому:

    Simple simp = new Simple();
    try
    {
        Console.WriteLine("here");
    }
    finally
    {
        if(simp != null)
        {
            simp.Dispose();
        }
    }

и вопрос в том, какого черта он проверяет null в finally? Блок finally будет выполняться только в том случае, если выполняется блок try, а блок try будет выполняться только в том случае, если конструктор Simple завершается успешно (т. е. не генерирует исключение), и в этом случае simp будет ненулевым. (Если есть некоторый страх, что между конструктором Simple и началом блока try могут возникнуть какие-то промежуточные шаги, то это действительно будет проблемой, потому что тогда может быть выброшено исключение, которое вообще предотвратит выполнение блока finally.) Так почему, черт возьми?

Отложив в сторону (пожалуйста) аргумент о том, что оператор using лучше, чем try-finally, я пишу свои блоки try-finally следующим образом:

    Simple simp = new Simple();
    try
    {
        Console.WriteLine("here");
    }
    finally
    {
        simp.Dispose();
        simp = null;        // sanity-check in case I touch simp again
                            // because I don't rely on all classes
                            // necessarily throwing
                            // ObjectDisposedException
    }

person Matthew    schedule 03.06.2009    source источник
comment
Меня заинтересовал один момент: насколько затратна эта дополнительная проверка работоспособности (simp = null) по сравнению с проверкой работоспособности, сгенерированной компилятором, с точки зрения производительности? В конце концов, разница между этими двумя кажется скорее философской, чем практической, но я могу ошибаться. Интересная дискуссия в любом случае.   -  person Fredrik Mörk    schedule 04.06.2009
comment
@Fredrik - Итак, вы спрашиваете, быстрее / медленнее ли установка нуля, чем сравнение с нулем? Я не уверен. Помимо этого, одним из преимуществ оператора using является то, что вам не нужно беспокоиться о доступе к этому объекту за пределами области использования. (Если у вас нет другой ссылки на него.)   -  person dss539    schedule 04.06.2009
comment
почему, черт возьми, он проверяет null в finally? Нет веской причины. Пропуск нулевой проверки — это оптимизация, которую мы могли бы выполнить. Мы этого не сделали. Ничего особенного; Нулевые чеки короткие и дешевые.   -  person Eric Lippert    schedule 04.06.2009
comment
Между прочим, есть ряд мест, где компилятор C# выполняет подобные микрооптимизации, если известно, что выражение не равно null, потому что оно является результатом нового выражения. Это как раз то, что мы пропустили.   -  person Eric Lippert    schedule 04.06.2009
comment
Я кратко обсуждаю эту оптимизацию здесь: blogs.msdn.com/ericlippert/archive/2009/06/11/   -  person Eric Lippert    schedule 11.06.2009


Ответы (5)


Нет, блок finally будет выполняться ВСЕГДА. Возможно, вы получаете объект не из новой, а из какой-то другой функции, которая возвращает ваш объект, и она может возвращать NULL. using() — ваш друг!

dss539 был достаточно любезен, чтобы предложить мне включить его примечание:

using(Simple simp = null) 

это еще одна причина, по которой расширение должно сначала проверять значение null.

person n8wrl    schedule 03.06.2009
comment
Просто чтобы немного расширить, вы можете использовать шаблон Factory и сделать что-то вроде использования (myFactory.CreateMyObject()), что может привести к нулевому объекту. - person Max Schmeling; 04.06.2009
comment
Однако, если конструктор терпит неудачу (до попытки), finally не будет выполнен. - person Zifre; 04.06.2009
comment
Предполагается, что «новый» вызовет исключение в случае сбоя, поэтому никогда не возвращает значение Null. Если он выдает исключение, блок try даже не будет достигнут, а блок finally не будет выполнен. Итак, блок finally не ВСЕГДА выполняется. - person spoulson; 04.06.2009
comment
@spoulson & @Zifre: Вы правы - try {} должен хотя бы выполниться. Но использование() предназначено для общего случая. Это может быть не новый оператор — как указывает Макс, это может быть фабрика или какой-то другой метод, который может возвращать значение null. если это не удается, не пытайтесь {}. Если это удается или возвращает ноль, будет выполнена попытка. - person n8wrl; 04.06.2009
comment
@ n8 - вы должны отредактировать мой ответ в своем, чтобы ОП мог принять ваш как полный ответ. - person dss539; 04.06.2009
comment
блок finally не всегда будет выполняться. в особых обстоятельствах (т. е. сбои программы, сбои, отключение питания) код в finally не будет выполняться: поэтому не помещайте никакую важную информацию в блок finally и ожидайте, что он всегда будет выполняться :) - person Makach; 04.06.2009
comment
Это неправильно. Если вы никогда не войдете в попытку, finally не будет выполнен. - person Robin Clowers; 01.07.2009

using(Simple simp = null) — это еще еще одна причина, по которой раскрытие должно сначала проверять значение null.

person dss539    schedule 03.06.2009
comment
Почему это вообще компилируется? Гарантируется, что это ошибка времени выполнения. - person Jonathan Allen; 04.06.2009
comment
Это гарантированно будет ошибкой времени выполнения только в том случае, если вы используете объект simp. Конечно, нет причин для использования, если вы этого не сделаете .... - person Max Schmeling; 04.06.2009
comment
И хотя маловероятно, что вы будете кодировать = null явно, это может быть = SomeFactoryMethod(), который может вернуть null. - person n8wrl; 04.06.2009
comment
используя (Simple simp = null) { if (someCondition) simp = new Simple(1); else if (otherCondition) simp = new Simple(2); ... } избавится от simp в любом случае, если он существует. - person configurator; 04.06.2009
comment
@configurator - правда, но это все еще немного ... опасно. простой = новый простой(); а затем еще один simp = new Simple(); и это будет только распоряжаться одним из них - person dss539; 04.06.2009
comment
... и он не компилируется: ошибка CS1656: невозможно назначить «simp», потому что это «использующая переменная» - person Fredrik Mörk; 04.06.2009
comment
@Фредрик - спасибо. поэтому компилятор применяет к нему некоторую форму инвариантности либо по необходимости, либо по доброте душевной. - person dss539; 04.06.2009
comment
Также допустимо (компилируемо) сказать: using ((Simple)null) { } - person Jeppe Stig Nielsen; 29.04.2013
comment
@MaxSchmeling нет причин для использования, если вы этого не сделаете: побочные эффекты. Конструктор объекта и метод dispose могут иметь побочные эффекты, такие как запуск и остановка удаленной службы, которая вызывается внутри используемого блока. Плохой дизайн, конечно, но возможная причина. - person phoog; 21.12.2018
comment
@phoog это хороший момент. Я также обнаружил, что полагаюсь на использование блоков, например, для получения и освобождения замков. - person dss539; 21.12.2018

MSDN в операторе using.

Что мне кажется странным, так это то, что он не расширяется до:

Simple simp = new Simple();
Simple __compilergeneratedtmpname = simp;
try
{
    Console.WriteLine("here");
}
finally
{
    if(__compilergeneratedtmpname != null)
    {
        __compilergeneratedtmpname.Dispose();
    }
}
person Dolphin    schedule 03.06.2009
comment
Вы хотели бы иметь возможность изменять переменную simp в используемом блоке?? - person Tom Hawtin - tackline; 04.06.2009
comment
Я не понимал, что компилятор применяет simp так же эффективно только для чтения. - person Dolphin; 05.06.2009
comment
Дельфин: Это вызовет проблемы при многопоточности. Когда за конструктором сразу же следует попытка, CLR гарантирует, что ни один поток не «прервется» посередине. По-вашему, нет никакой гарантии. - person configurator; 05.06.2009
comment
@configurator: Не понял! - person Jimmy; 12.06.2009

Похоже, что ваш комментарий:

«Если есть некоторый страх, что между конструктором Simple и началом блока try могут возникнуть какие-то промежуточные шаги, то это действительно будет проблемой, потому что тогда может быть выброшено исключение, которое вообще помешает выполнению блока finally».

возможно мертв. Видеть:

Ошибки атомарности и асинхронных исключений

Я также хочу отметить проблемы с WCF и использованием:

Избежание проблем с оператором использования и прокси-серверами службы WCF, который ссылается на:

Как избежать проблем с оператором Using

person Joshua Drake    schedule 23.03.2010

Код должен быть переведен таким образом, чтобы избежать возможного NullReferenceException при удалении объекта. Согласно справочнику по языку C#, используя оператор принимает не только объявление локальной переменной в качестве первого нетерминального символа resource_acquisition, но и любое выражение. Рассмотрим следующий код:

DisposableType @object = null;
using(@object) {
    // whatever
}

Очевидно, что если в блоке finnaly не будет @object?.Dispose() с нулевым условием, возникнет исключение. Проверка на значение NULL является лишней только в том случае, если выражение имеет тип значения, не допускающий значение NULL (структура, не допускающая значение NULL). И действительно, согласно вышеупомянутой языковой справке, в таком случае его нет.

person Krzysztof Mierzejewski    schedule 01.07.2020