Может ли выражение (plain) throw в С# вызывать исключения? - программирование
Подтвердить что ты не робот

Может ли выражение (plain) throw в С# вызывать исключения?

Вопрос: Может ли простой оператор throw в С# когда-либо вызывать новое исключение?


Обратите внимание, что я задаю этот вопрос из любопытства, а не потому, что у меня есть практическая или реальная ситуация, когда это имеет большое значение. Также обратите внимание, что мои чувства и опыт подсказывают мне, что ответ "Нет", но я хочу как-то проверить этот ответ (см. Далее источники, которые я пробовал до сих пор).

Вот пример кода для иллюстрации моего вопроса:

try
{
    int x = 0, y = 1 / x;
}
catch (Exception outerException)
{

    try
    {
        throw;
    }
    catch (Exception innerException)
    {
        // Q: Does this Assert ever fail??
        System.Diagnostics.Debug.Assert(outerException.Equals(innerException));
    }
}

Мне интересно, есть ли вообще способ изменить обстоятельства, при которых Assert завершится с ошибкой, не касаясь внутреннего блока try/catch.

Что я пробовал или пытался ответить на это:

  • Прочитайте бросок (ссылка на С#) на MSDN - нет окончательного ответа;
  • Проверенная часть 5.3.3.11 Спецификация языка С# - это, вероятно, неправильное место для поиска такого рода информации;
  • Глянцевать через исключения, которые я мог бы попытаться вызвать в команде throw. Исключение OutOfMemoryException приходит на ум, но его трудно вызвать во время throw.
  • Открыл ILDASM, чтобы проверить сгенерированный код. Я вижу, что throw преобразуется в инструкцию rethrow, но я теряюсь там, где нужно посмотреть, чтобы проверить, может ли этот оператор или не может выполнить исключение.

Это то, что ILDASM показывает для внутреннего бита try:

.try
{
  IL_000d:  nop
  IL_000e:  rethrow
}  // end .try

Итак, чтобы суммировать: может ли оператор throw (используемый для перебора исключения) когда-либо вызывать само исключение?

4b9b3361

Ответ 1

По моему честному мнению, теоретически утверждение может "потерпеть неудачу" (практически я так не думаю).

Как?

Примечание. Ниже приведены только мои "мнения" на основе некоторых исследований, которые я ранее сделал в SSCLI.

  • Может возникнуть InvalidProgramException. Это, по общему признанию, крайне маловероятно, но, тем не менее, теоретически возможно (например, некоторая внутренняя ошибка CLR может привести к тому, что бросающийся объект станет недоступным!!!!).
  • Если CLR не найдет достаточно памяти для обработки действия "повторного броска", вместо этого вместо него будет выбрано исключение OutOfMemoryException (внутренняя логика повторного броска CLR требует выделения некоторой памяти, если она не имеет отношения к "предварительно выделенным" исключениям, например OutOfMemoryException).
  • Если среда CLR запущена под каким-либо другим хостом (например, SQL-сервер или даже ваш собственный), и хост решает прекратить поток повторного броска исключения (на основе некоторой внутренней логики) ThreadAbortException (известный как прерывание отбойного потока в этом случае). Хотя, я не уверен, что Assert даже выполнит в этом случае.
  • Пользовательский хост, возможно, применил политику эскалации к CLR (ICLRPolicyManager:: SetActionOnFailure). В этом случае, если вы имеете дело с OutOfMemoryException, политика эскалации может привести к возникновению ThreadAbortException (снова грубый поток прерывается. Не уверен, что произойдет, если политика диктует нормальный поток прерывания).
  • Хотя @Alois Kraus поясняет, что исключение "нормального" прерывания потока невозможно, из исследования SSCLI я все еще сомневаюсь, что может произойти (нормальное) ThreadAbortException.

Edit:

Как я уже говорил, утверждение может терпеть неудачу теоретически, но практически маловероятно. Поэтому для этого очень сложно разработать POC. Чтобы предоставить больше "доказательств", следуют фрагменты кода SSCLI для обработки инструкции rethow IL, которые подтверждают мои вышеуказанные баллы.

Предупреждение. Коммерческая среда CLR может сильно отличаться от SSCLI.

  • InvalidProgramException:

    if (throwable != NULL)
    {
     ...
    }
    else
    {
        // This can only be the result of bad IL (or some internal EE failure).
        RealCOMPlusThrow(kInvalidProgramException, (UINT)IDS_EE_RETHROW_NOT_ALLOWED);
    }
    
  • Отказоустойчивость:

    if (pThread->IsRudeAbortInitiated())
    {
        // Nobody should be able to swallow rude thread abort.
        throwable = CLRException::GetPreallocatedRudeThreadAbortException();
    }
    

    Это означает, что если был запущен "грубый откат нити", любое исключение будет изменено на исключение из строгой отмены прерывания.

  • Теперь самое интересное, OutOfMemoryException. Поскольку команда rethrow IL по существу перебрасывает один и тот же объект Exception (т.е. object.ReferenceEquals возвращает true), кажется невозможным, что OutOfMemoryException может возникать при повторном броске. Однако, следующий код SSCLI показывает, что это возможно:

     // Always save the current object in the handle so on rethrow we can reuse it. This is important as it
    // contains stack trace info.
    //
    // Note: we use SafeSetLastThrownObject, which will try to set the throwable and if there are any problems,
    // it will set the throwable to something appropiate (like OOM exception) and return the new
    // exception. Thus, the user exception object can be replaced here.
    
    throwable = pThread->SafeSetLastThrownObject(throwable);
    

    SafeSetLastThrownObject вызывает SetLastThrownObject, и если он терпит неудачу, возникает OutOfMemoryException. Вот фрагмент от SetLastThrownObject (добавлены мои комментарии)

    ...
    if (m_LastThrownObjectHandle != NULL)
    {
       // We'll somtimes use a handle for a preallocated exception object. We should never, ever destroy one of
      // these handles... they'll be destroyed when the Runtime shuts down.
      if (!CLRException::IsPreallocatedExceptionHandle(m_LastThrownObjectHandle))
      {
         //Destroys the GC handle only but not the throwable object itself
         DestroyHandle(m_LastThrownObjectHandle);
      }
    }
    ...
    
    //This step can fail if there is no space left for a new handle
    m_LastThrownObjectHandle = GetDomain()->CreateHandle(throwable);
    

    Над фрагментами кода показано, что дескриптор GC дескриптора объекта уничтожается (т.е. освобождает слот в таблице GC), а затем создается новый дескриптор. Поскольку слот только что был выпущен, новое создание дескриптора никогда не завершится до тех пор, пока не пройдет курс в очень редком сценарии нового потока, который будет запланирован только в нужное время и будет потреблять все доступные GC-ручки.

Кроме того, все исключения (в том числе rethrows) поднимаются через RaiseException win api. Код, который ловит это исключение для подготовки соответствующего управляемого исключения, сам может поднять OutOfMemoryException.

Ответ 2

Может ли простой оператор throw в С# когда-либо вызывать новое исключение?

По определению это не будет. Сама точка throw; заключается в том, чтобы сохранить активное исключение (особенно трассировку стека).

Теоретически реализация могла бы клонировать исключение, но какова была бы точка?

Ответ 3

Я подозреваю, что бит, который вам не хватает, может быть спецификацией rethrow, которая находится в ECMA-335, раздел III, раздел 4,24:

4.24 rethrow - реконструировать текущее исключение

Описание:
Инструкция rethrow разрешена только в теле обработчика catch (см. Раздел I). Он выделяет ту же исключение, которая была захвачена этим обработчиком. Ретрол не изменяет трассировку стека в объекте.

Исключения:
Исходное исключение выбрано.

(Акцент мой)

Итак, похоже, что ваше утверждение гарантированно работает в соответствии со спецификацией. (Конечно, это предполагает, что реализация следует за спецификацией...)

Соответствующая часть спецификации С# - это раздел 8.9.5 (версия С# 4):

Оператор throw без выражения может использоваться только в блоке catch, и в этом случае этот оператор повторно генерирует исключение, которое в настоящее время обрабатывается этим блоком catch.

Что опять же предполагает, что будет выбрано исходное исключение и только это исключение.

(раздел 5.3.3.11, о котором вы говорили, просто говорит об определенном назначении, а не о поведении самого оператора throw.)

Ничто из этого не отменяет точки Амита, конечно, для ситуаций, которые несколько выходят за рамки того, что указано в любом месте. (Когда хосты применяют дополнительные правила, для спецификации языка трудно учесть их.)

Ответ 4

Ваше утверждение никогда не потерпит неудачу, потому что нет никакого кода между ретровом и утверждением. Единственный случай, когда исключение изменяется, если вы поймаете исключение и вызывают другое - например. с помощью багги-кода или "throw new" в вашем catch,

Ответ 5

В сочетании с обычной рекурсией throw можно легко вызвать StackOverflowException на 64-разрядных платформах.

class Program
{
    // expect it to be 10 times less in real code
    static int max = 455;

    static void Test(int i)
    {
        try {
            if (i >= max) throw new Exception("done");
            Test(i + 1);
        }
        catch {
            Console.WriteLine(i);
            throw;
        }
    }

    static void Main(string[] args)
    {
        try {
            Test(0);
        }
        catch {
        }
        Console.WriteLine("Done.");
    }
}

В консоли:

...
2
1
0

Process is terminated due to StackOverflowException.

Некоторые объяснения можно найти здесь.