Как предложения CIL 'fault' отличаются от предложений 'catch' на С#? - программирование

Как предложения CIL 'fault' отличаются от предложений 'catch' на С#?

В соответствии с стандартом CLI (раздел IIA, глава 19) и справочной страницей MSDN для System.Reflection.ExceptionHandlingClauseOptions enum, существует четыре разных типа блоков обработчиков исключений:

  • catch: "Поймать все объекты указанного типа".
  • фильтр: "Введите обработчик только в том случае, если фильтр успешно завершен".
  • finally: "Обработать все исключения и нормальный выход".
  • fault: "Обработка всех исключений, но не нормальный выход".

Учитывая эти краткие объяснения (цитируемые из стандарта CLI, кстати), они должны отображаться на С# следующим образом:

  • catchcatch (FooException) { … }
  • фильтр — недоступен в С# (но в VB.NET как Catch FooException When booleanExpression)
  • наконецfinally { … }
  • ошибкаcatch { … }

Эксперимент:

Простой эксперимент показывает, что это сопоставление не является компилятором .NET С#:

// using System.Linq;
// using System.Reflection;

static bool IsCatchWithoutTypeSpecificationEmittedAsFaultClause()
{
    try
    {
        return MethodBase
               .GetCurrentMethod()
               .GetMethodBody()
               .ExceptionHandlingClauses
               .Any(clause => clause.Flags == ExceptionHandlingClauseOptions.Fault);
    }
    catch // <-- this is what the above code is inspecting
    {
        throw;
    }
}

Этот метод возвращает false. То есть catch { … } не выбрасывается как предложение о неисправности.

Аналогичный эксперимент показывает, что на самом деле было предложено предложение catch (clause.Flags == ExceptionHandlingClauseOptions.Clause), хотя тип исключения не указан.

Вопросы:

  • Если catch { … } действительно является предложением catch, тогда как предложения о повреждениях отличаются от предложений catch?
  • Компилятор С# когда-либо выводит предложения о неисправности?
4b9b3361

Ответ 1

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

  • catch: "Поймать все объекты указанного типа".
  • фильтр: "Введите обработчик только в том случае, если фильтр успешно завершен".
  • finally: "Обработать все исключения и нормальный выход".
  • fault: "Обработка всех исключений, но не нормальный выход".

Учитывая эти краткие объяснения (цитируемые из стандарта CLI, кстати), они должны отображаться на С# следующим образом:

  • catchcatch (FooException) { … }
  • фильтр — недоступен в С# (но в VB.NET как Catch FooException When booleanExpression)
  • наконецfinally { … }
  • ошибкаcatch { … }

Это последняя строка, где вы поступили не так. Прочтите описания еще раз. fault и finally описаны практически одинаково. Разница между ними заключается в том, что finally всегда вводится, а fault вводится только в том случае, если элемент управления try исключается. Обратите внимание, что это означает, что блок catch, возможно, уже действовал.

Если вы пишете это в С#:

try {
    ...
} catch (SpecificException ex) {
    ...
} catch {
    ...
}

Тогда нет никакого способа, чтобы третий блок был введен, если управление оставляет try через a SpecificException. Поэтому catch {} не является отображением для fault.

Ответ 2

1. Если catch { … } действительно является предложением catch, то как предложения о повреждениях отличаются от предложений catch?

Компилятор С# (по крайней мере, тот, который поставляется с .NET), по-видимому, компилирует catch { … }, как если бы он действительно был catch (object) { … }. Это можно показать с помощью кода ниже.

// using System;
// using System.Linq;
// using System.Reflection;

static Type GetCaughtTypeOfCatchClauseWithoutTypeSpecification()
{
    try
    {
        return MethodBase
               .GetCurrentMethod()
               .GetMethodBody()
               .ExceptionHandlingClauses
               .Where(clause => clause.Flags == ExceptionHandlingClauseOptions.Clause)
               .Select(clause => clause.CatchType)
               .Single();
    }
    catch // <-- this is what the above code is inspecting
    {
        throw;
    }
}

Этот метод возвращает typeof(object).

Итак, концептуально, обработчик ошибок is   похож на a catch { … }; однако компилятор С# никогда не генерирует код для этой точной конструкции, но делает вид, что это catch (object) { … }, который концептуально является предложением catch. Таким образом, выдается предложение catch.

Примечание: книга Джеффри Рихтера "CLR via С#" имеет некоторую связанную информацию (на стр. 472 – 474): А именно, что CLR позволяет вызывать любое значение, а не только объекты Exception. Однако, начиная с версии CLR версии 2, значения не Exception автоматически обертываются в объект RuntimeWrappedException. Поэтому кажется несколько неожиданным, что С# преобразует catch в catch (object) вместо catch (Exception). Однако есть причина для этого: CLR может быть сказано не переносить значения не Exception, применяя атрибут [assembly: RuntimeCompatibility(WrapNonExceptionThrows = false)].

Кстати, компилятор VB.NET, в отличие от компилятора С#, переводит catch в Catch anonymousVariable As Exception.


2. Разве компилятор С# когда-либо выводит предложения о неисправности?

Он явно не выделяет предложения об ошибке для catch { … }. Тем не менее, сообщение в блоге Барта де Смета "Задача читателя – обработчики ошибок на С#" предполагает, что Компилятор С# в определенных обстоятельствах создает предложения о повреждениях.

Ответ 3

Исключения .NET с возвратом на поддержку операционной системы для исключений. Вызывается обработка структурированных исключений в Windows. Операционные системы Unix имеют нечто похожее, сигналы.

Управляемое исключение - очень конкретный случай исключения SEH. Код исключения - 0xe0434f53. Последние три шестнадцатеричных пары произносят "COM", сообщает вам что-то о том, как начал .NET.

В целом программа может заинтересоваться тем, когда возникает и обрабатывается какое-либо исключение, а не только управляемые исключения. Это можно увидеть и в компиляторе MSVC С++. Предложение catch (...) допускает исключения С++. Но если вы скомпилируете с параметром /EHa, то он поймает любое исключение. Включая действительно неприятные вещи, исключения процессора, такие как нарушения доступа.

Предложение о неисправности - это версия CLR, связанный с ним блок будет выполняться для любого исключения операционной системы, а не только для управляемых. Языки С# и VB.NET не поддерживают это, они поддерживают только обработку исключений для управляемых исключений. Но другие языки могут, я знаю только компилятор С++/CLI, испускающий их. Сделано, например, в его версии инструкции using, называемой "семантика стека".

Имеет смысл, что С++/CLI будет поддерживать это, это, в конце концов, язык, который сильно поддерживает прямой вызов собственного кода из управляемого кода. Но не для С# и VB.NET, они только запускают неуправляемый код через маркерщик pinvoke или слой взаимодействия COM в среде CLR. Который уже устанавливает обработчик "catch-them-all", который переводит неуправляемые исключения в управляемые. Каков механизм, с помощью которого вы получаете исключение System.AccessViolationException.

Ответ 4

Как отмечают люди, вообще говоря, компилятор С# не генерирует обработчики ошибок. Тем не менее, stakx связан с сообщением блога Bart de Smet о том, как заставить компилятор С# генерировать обработчик ошибок.

С# использует обработчики ошибок для реализации с использованием операторов, которые находятся внутри блоков итератора. Например, следующий код С# заставит компилятор использовать предложение о неисправности:

public IEnumerable<string> GetSomeEnumerable()
{
    using (Disposable.Empty)
    {
        yield return DoSomeWork();
    }
}

Декомпиляция сгенерированной сборки с помощью функции dotPeek и опции "Показывать сгенерированный компилятором" вы можете увидеть предложение о неисправности:

bool IEnumerator.MoveNext()
{
    try
    {
        switch (this.<>1__state)
        {
        case 0:
            this.<>1__state = -1;
            this.<>7__wrap1 = Disposable.Empty;
            this.<>1__state = 1;
            this.<>2__current = this.<>4__this.DoSomeWork();
            this.<>1__state = 2;
            return true;
        case 2:
            this.<>1__state = 1;
            this.<>m__Finally2();
            break;
        }
        return false;
    }
    __fault
    {
        this.System.IDisposable.Dispose();
    }
}

Где обычно оператор using будет сопоставлять блок try/finally, это не имеет смысла для блоков итератора - try/finally будет Dispose после создания первого значения.

Но если DoSomeWork выдает исключение, вы хотите Dispose. Поэтому здесь полезен обработчик ошибок. Он будет вызывать Dispose только в том случае, когда возникает исключение, и разрешить исключение. Концептуально это похоже на блокирующий блок, который размещает, а затем повторно бросает.

Ответ 5

Блок сбоев будет эквивалентен:

bool success;
try
{
    success = false;
    ... do stuff
    success = true; // Also include this immediately before any 'return'    
}
finally
{
    if (!success)
    {
        ... do "fault" stuff here
    }
}

Обратите внимание, что это несколько отличается семантически от catch-and-rethrow. Среди прочего, с реализацией выше, если возникает исключение, и трассировка стека сообщает номера строк, она будет содержать номер строки в ...do stuff, где произошло исключение. В отличие от этого, при использовании catch-and-rethrow, трассировка стека будет сообщать номер строки ретрона. Если ...do stuff включает в себя два или более вызовов на foo, и один из этих вызовов вызывает исключение, зная, что номер строки отказавшего вызова может оказаться полезным, но catch-and-rethrow потеряет эту информацию.

Самые большие проблемы с вышеприведенной реализацией заключаются в том, что нужно вручную добавить success = true; в каждое место в коде, который может выйти из блока try, и что нет способа для блока finally знать, какое исключение может быть в ожидании. Если бы у меня были мои druthers, был бы оператор finally (Exception ex), который установил бы Ex в исключение, которое заставило блок try выйти (или null, если бы блок вышел из него нормально). Это не только устранило бы необходимость вручную установить флаг "успех", но это позволило бы разумно обрабатывать случай, когда в коде очистки возникает исключение. В такой ситуации не следует скрывать исключение очистки (даже если исходное исключение обычно представляет собой условие, которое ожидал код вызова, ошибка очистки, вероятно, представляет собой условие, которого оно не было), но, вероятно, не хочет терять Исходное исключение (так как оно, вероятно, содержит сведения о том, почему очистка не удалась). Разрешить блоку finally знать, почему он был введен, и включить расширенную версию IDisposable, через которую блок using мог бы сделать такую ​​информацию доступной для кода очистки, позволил бы чисто разрешить такие ситуации.