Обработка исключений, выбрасываемых "Dispose" при разматывании вложенных "использования" операторов

По-видимому, некоторые исключения могут просто потеряться при использовании вложенного оператора using. Рассмотрим это простое консольное приложение:

using System;

namespace ConsoleApplication
{
    public class Throwing: IDisposable
    {
        int n;

        public Throwing(int n)
        {
            this.n = n;
        }

        public void Dispose()
        {
            var e = new ApplicationException(String.Format("Throwing({0})", this.n));
            Console.WriteLine("Throw: {0}", e.Message);
            throw e;
        }
    }

    class Program
    {
        static void DoWork()
        {
            // ... 
            using (var a = new Throwing(1))
            {
                // ... 
                using (var b = new Throwing(2))
                {
                    // ... 
                    using (var c = new Throwing(3))
                    {
                        // ... 
                    }
                }
            }
        }

        static void Main(string[] args)
        {
            AppDomain.CurrentDomain.UnhandledException += (sender, e) =>
            {
                // this doesn't get called
                Console.WriteLine("UnhandledException:", e.ExceptionObject.ToString());
            };

            try
            {
                DoWork();
            }
            catch (Exception e)
            {
                // this handles Throwing(1) only
                Console.WriteLine("Handle: {0}", e.Message);
            }

            Console.ReadLine();
        }
    }
}

Каждый экземпляр Throwing бросает, когда он удаляется. AppDomain.CurrentDomain.UnhandledException никогда не вызывается.

Выход:

Throw: Throwing(3)
Throw: Throwing(2)
Throw: Throwing(1)
Handle: Throwing(1)

Я предпочитаю, по крайней мере, иметь возможность регистрировать недостающие Throwing(2) и Throwing(3). Как это сделать, не прибегая к отдельному try/catch для каждого using (что бы убить удобство using)?

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

Этот вопрос возник, пока я смотрел на уменьшая уровень вложенных using. Там аккуратный ответ, предлагающий агрегирование исключений. Интересно, как это отличается от стандартного поведения вложенных операторов using.

[EDITED] Этот вопрос, по-видимому, тесно связан: Следует ли реализовать IDisposable.Dispose(), чтобы он никогда не выбрасывал?

Ответ 1

То, что вы заметили, является фундаментальной проблемой при проектировании Dispose и using, для которых пока нет хорошего решения. IMHO лучшим вариантом было бы иметь версию Dispose, которая получает в качестве аргумента любое исключение, которое может быть отложено (или null, если ни один не находится в ожидании), и может либо регистрировать, либо инкапсулировать это исключение, если ему нужно выбросить один из своих. В противном случае, если у вас есть контроль над кодом, который может вызвать исключение в using, а также в Dispose, вы можете использовать какой-то внешний канал данных, чтобы Dispose знал о внутреннее исключение, но это скорее hokey.

Слишком плохо, что нет надлежащей поддержки языка для кода, связанного с блоком finally (явно или неявно через using), чтобы узнать, правильно ли выполняется связанный try, а если нет, что пошло не так. Понятие, что Dispose должно терпеть неудачу, ИМХО очень опасно и неправильно. Если объект инкапсулирует файл, открытый для записи, и Dispose закрывает файл (общий шаблон), и данные не могут быть записаны, при условии, что возврат вызова Dispose обычно приведет к тому, что вызывающий код верят, что данные были написаны правильно, потенциально позволяя ему перезаписать единственную хорошую резервную копию. Кроме того, если файлы должны быть явно закрыты и вызов Dispose без закрытия файла должен считаться ошибкой, это означает, что Dispose должен вызывать исключение, если защищенный блок в противном случае завершил бы нормально, но если защищенный блок не вызвал Close, потому что сначала возникло исключение, причем Dispose throw исключение было бы очень бесполезным.

Если производительность не является критичной, вы можете написать метод оболочки в VB.NET, который принял бы два делегата (из типов Action и Action<Exception>), вызовет первый в блоке try, а затем вызовите второй в блоке finally с исключением, произошедшим в блоке try (если есть). Если метод-оболочка был написан на VB.NET, он мог бы обнаружить и сообщить об исключении, которое не произошло, чтобы не поймать и не восстановить его. Другие шаблоны были бы возможны. Большинство применений обертки будут связаны с закрытием, которые являются icky, но оболочка может по крайней мере достичь правильной семантики.

Альтернативная конструкция обертки, которая позволит избежать замыканий, но потребует, чтобы клиенты правильно ее использовали и обеспечили бы небольшую защиту от неправильного использования, использовали бы тестовое использование, например:

var dispRes = new DisposeResult();
... 
try
{
  .. the following could be in some nested routine which took dispRes as a parameter
  using (dispWrap = new DisposeWrap(dispRes, ... other disposable resources)
  {
    ...
  }
}
catch (...)
{
}
finally
{
}
if (dispRes.Exception != null)
  ... handle cleanup failures here

Проблема с этим подходом заключается в том, что нет возможности гарантировать, что кто-либо когда-либо оценит dispRes.Exception. Можно использовать финализатор для регистрации случаев, когда dispRes получает отказ, даже если его не проверяли, но не было возможности различать случаи, когда это произошло, потому что исключение выкидывало код вне теста if или потому, что программист просто забыли проверить.

PS - Другой случай, когда Dispose действительно должен знать, происходят ли исключения, когда объекты IDisposable используются для переноса блокировок или других областей, где инварианты объектов могут временно быть недействительными, но ожидается, что они будут восстановлены до того, как код покинет объем. Если возникает исключение, код часто не должен рассчитывать на устранение исключения, но должен тем не менее принимать меры на его основе, оставляя блокировку не сохраненной и не выпущенной, а скорее недействительной, так что любая настоящая или будущая попытка ее приобретения выдает исключение, Если в будущем нет попыток получить блокировку или другой ресурс, тот факт, что он недействителен, не должен нарушать работу системы. Если ресурс критически необходим для какой-либо части программы, ее недействительность приведет к тому, что эта часть программы умрет при минимизации ущерба, который она наносит другому. Единственный способ, которым я знаю, действительно реализовать этот случай с хорошей семантикой, - это использовать нечеткие замыкания. В противном случае единственная альтернатива - требовать явных недействительных/проверенных вызовов и надеяться, что любые операторы возврата внутри части кода, где ресурс недействителен, предшествуют вызовам для проверки.

Ответ 2

Для этого есть предупреждение анализатора кода. CA1065, "Не создавать исключения в неожиданных местах". Метод Dispose() находится в этом списке. Также сильное предупреждение в Руководстве по проектированию рамок, глава 9.4.1:

ИЗБЕГАЙТЕ исключить исключение из Dispose (bool), за исключением критических ситуаций, когда содержащийся процесс был поврежден (утечки, несогласованное общее состояние и т.д.).

Это происходит не так, потому что оператор using вызывает Dispose() внутри блока finally. Исключение, возникающее в блоке finally, может иметь неприятный побочный эффект, он заменяет активное исключение, если был вызван блок finally, когда стек разматывается из-за исключения. Именно то, что вы видите, происходит здесь.

Код репрограммы:

class Program {
    static void Main(string[] args) {
        try {
            try {
                throw new Exception("You won't see this");
            }
            finally {
                throw new Exception("You'll see this");
            }
        }
        catch (Exception ex) {
            Console.WriteLine(ex.Message);
        }
        Console.ReadLine();
    }
}

Ответ 3

Может быть, какая-то вспомогательная функция, которая позволяет вам писать код, похожий на using:

 void UsingAndLog<T>(Func<T> creator, Action<T> action) where T:IDisposabe
 {  
      T item = creator();
      try 
      {
         action(item);
      }
      finally
      { 
          try { item.Dispose();}
          catch(Exception ex)
          {
             // Log/pick which one to throw.
          } 
      }      
 }

 UsingAndLog(() => new FileStream(...), item => 
 {
     //code that you'd write inside using 
     item.Write(...);
 });

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