Перехват исключения внутри IDisposable.Dispose
В IDisposable.Dispose
Есть ли способ выяснить, генерируется ли исключение?
using (MyWrapper wrapper = new MyWrapper())
{
throw new Exception("Bad error.");
}
Если исключение выдается в using
заявление, которое я хочу знать об этом, когда IDisposable
Объект расположен.
11 ответов
Нет, в.Net Framework это сделать невозможно, вы не можете определить текущее исключение, которое выдается в предложении finally.
См. Этот пост в моем блоге, для сравнения с аналогичным шаблоном в Ruby, он выделяет пробелы, которые, я думаю, существуют в шаблоне IDisposable.
У Айенде есть хитрость, которая позволит вам обнаружить возникшее исключение, однако не скажет вам, какое это было исключение.
Вы можете продлить IDisposable
с методом Complete
и использовать шаблон так:
using (MyWrapper wrapper = new MyWrapper())
{
throw new Exception("Bad error.");
wrapper.Complete();
}
Если исключение выбрасывается внутри using
заявление Complete
не будет вызван раньше Dispose
,
Если вы хотите узнать, какое именно исключение выдается, подпишитесь на AppDomain.CurrentDomain.FirstChanceException
событие и сохранить последнее выброшенное исключение в ThreadLocal<Exception>
переменная.
Такой шаблон реализован в TransactionScope
учебный класс.
Невозможно зафиксировать исключение в Dispose()
метод.
Тем не менее, это можно проверить Marshal.GetExceptionCode()
в Dispose, чтобы обнаружить, если исключение действительно произошло, но я бы не стал полагаться на это.
Если вам не нужен класс и вы хотите просто захватить исключение, вы можете создать функцию, которая принимает лямбду, которая выполняется в блоке try/catch, что-то вроде этого:
HandleException(() => {
throw new Exception("Bad error.");
});
public static void HandleException(Action code)
{
try
{
if (code != null)
code.Invoke();
}
catch
{
Console.WriteLine("Error handling");
throw;
}
}
Например, вы можете использовать метод, который автоматически выполняет Commit() или Rollback() транзакции и делает некоторую запись в журнал. В этом случае вам не всегда нужен блок try/catch.
public static int? GetFerrariId()
{
using (var connection = new SqlConnection("..."))
{
connection.Open();
using (var transaction = connection.BeginTransaction())
{
return HandleTranaction(transaction, () =>
{
using (var command = connection.CreateCommand())
{
command.Transaction = transaction;
command.CommandText = "SELECT CarID FROM Cars WHERE Brand = 'Ferrari'";
return (int?)command.ExecuteScalar();
}
});
}
}
}
public static T HandleTranaction<T>(IDbTransaction transaction, Func<T> code)
{
try
{
var result = code != null ? code.Invoke() : default(T);
transaction.Commit();
return result;
}
catch
{
transaction.Rollback();
throw;
}
}
Джеймс, все wrapper
может сделать, это зарегистрировать свои собственные исключения. Вы не можете заставить потребителя wrapper
регистрировать свои собственные исключения. Это не то, что IDisposable для. IDisposable предназначен для полудетерминированного высвобождения ресурсов для объекта. Написание правильного кода IDisposable не тривиально.
На самом деле, от потребителя класса даже не требуется вызывать метод утилизации ваших классов, и при этом они не обязаны использовать блок using, так что все это скорее сломается.
Если вы посмотрите на это с точки зрения класса-оболочки, почему это должно заботиться о том, чтобы он присутствовал внутри блока using и было исключение? Какие знания это приносит? Есть ли угроза безопасности, когда сторонний код становится доступным к деталям исключений и трассировке стека? Что можешь wrapper
делать, если в расчете есть деление на ноль?
Единственный способ записать исключения, независимо от IDisposable, - это выполнить try-catch, а затем повторно добавить функцию catch.
try
{
// code that may cause exceptions.
}
catch( Exception ex )
{
LogExceptionSomewhere(ex);
throw;
}
finally
{
// CLR always tries to execute finally blocks
}
Вы упоминаете, что создаете внешний API. Вам нужно будет обернуть каждый вызов на открытой границе вашего API с помощью try-catch, чтобы записать, что исключение пришло из вашего кода.
Если вы пишете общедоступный API, то вам действительно следует прочитать Руководство по проектированию платформы: условные обозначения, идиомы и шаблоны для многократно используемых библиотек.NET (Microsoft .NET Development Series) - 2-е издание.. 1-е издание.
Хотя я не защищаю их, я видел IDisposable, используемый для других интересных шаблонов:
- Семантика автооткатных транзакций. Класс транзакции откатит транзакцию на Dispose, если она еще не зафиксирована.
- Блоки временного кода для регистрации. Во время создания объекта была записана временная метка, а при утилизации был рассчитан интервал времени и записано событие журнала.
* Эти шаблоны могут быть достигнуты с помощью другого уровня косвенных и анонимных делегатов легко и без необходимости перегружать семантику IDisposable. Важным примечанием является то, что ваша оболочка IDisposable бесполезна, если вы или член команды забудете ее правильно использовать.
Вы можете сделать это, реализуя метод Dispose для класса "MyWrapper". В методе dispose вы можете проверить, есть ли исключение следующим образом
public void Dispose()
{
bool ExceptionOccurred = Marshal.GetExceptionPointers() != IntPtr.Zero
|| Marshal.GetExceptionCode() != 0;
if(ExceptionOccurred)
{
System.Diagnostics.Debug.WriteLine("We had an exception");
}
}
Вы можете не только узнать, было ли выброшено исключение при утилизации одноразового предмета, вы даже можете получить в руки исключение, которое было брошено внутрь предложения finally с небольшой магией. Моя библиотека трассировки инструмента ApiChange использует этот метод для отслеживания исключений внутри оператора using. Более подробную информацию о том, как это работает, можно найти здесь.
С уважением, Алоис Краус
В моем случае я хотел сделать это, чтобы регистрировать, когда происходит сбой микросервиса. У меня уже есть на месте using
правильно очистить прямо перед закрытием экземпляра, но если это из-за исключения, я хочу понять, почему, и я ненавижу нет ответа.
Вместо того, чтобы пытаться заставить его работать в Dispose()
, возможно, назначьте делегата для работы, которую вам нужно сделать, а затем оберните туда свой захват исключений. Поэтому в моем логгере MyWrapper я добавляю метод, который принимает Action / Func:
public void Start(Action<string, string, string> behavior)
try{
var string1 = "my queue message";
var string2 = "some string message";
var string3 = "some other string yet;"
behaviour(string1, string2, string3);
}
catch(Exception e){
Console.WriteLine(string.Format("Oops: {0}", e.Message))
}
}
Для реализации:
using (var wrapper = new MyWrapper())
{
wrapper.Start((string1, string2, string3) =>
{
Console.WriteLine(string1);
Console.WriteLine(string2);
Console.WriteLine(string3);
}
}
В зависимости от того, что вам нужно сделать, это может быть слишком ограничительным, но это сработало для того, что мне нужно.
Вместо синтаксического сахара оператора using почему бы просто не реализовать свою собственную логику для этого. Что-то вроде:
try
{
MyWrapper wrapper = new MyWrapper();
}
catch (Exception e)
{
wrapper.CaughtException = true;
}
finally
{
if (wrapper != null)
{
wrapper.Dispose();
}
}
Теперь, в 2017 году, это общий способ сделать это, включая обработку отката для исключений.
public static T WithinTransaction<T>(this IDbConnection cnn, Func<IDbTransaction, T> fn)
{
cnn.Open();
using (var transaction = cnn.BeginTransaction())
{
try
{
T res = fn(transaction);
transaction.Commit();
return res;
}
catch (Exception)
{
transaction.Rollback();
throw;
}
finally
{
cnn.Close();
}
}
}
и вы называете это так:
cnn.WithinTransaction(
transaction =>
{
var affected = ..sqlcalls..(cnn, ..., transaction);
return affected;
});
Если вы хотите остаться в.net, я бы предложил два подхода: написать оболочку "try-catch-finally", которая будет принимать делегатов для разных частей, или написать оболочку "use-style", которая принимает метод, который должен быть вызван, вместе с одним или несколькими IDisposable объектами, которые должны быть удалены после его завершения.
Оболочка "в стиле использования" может обрабатывать удаление в блоке try-catch и, если выбрасываются какие-либо исключения, либо оборачивать их в исключение CleanupFailureException, которое будет содержать ошибки удаления, а также любое исключение, которое произошло в главном делегате. или добавьте что-то в свойство "Данные" исключения с исходным исключением. Я бы предпочел обернуть вещи в исключение CleanupFailureException, поскольку исключение, возникающее во время очистки, обычно указывает на гораздо большую проблему, чем та, которая возникает при обработке основной строки; кроме того, CleanupFailureException может быть написано так, чтобы включать несколько вложенных исключений (если существует n объектов IDisposable, может быть n+1 вложенных исключений: одно от основной линии и одно от каждой утилизации).
Оболочка try-catch-finally, написанная на vb.net и вызываемая из C#, может включать некоторые функции, которые в противном случае недоступны в C#, в том числе возможность расширения до "try-filter-catch-fault-finally" блок, где код "фильтра" будет выполнен до того, как стек будет размотан из исключения, и определит, должно ли исключение быть перехвачено, блок "ошибка" будет содержать код, который будет выполняться только в случае возникновения исключения, но фактически не будет перехватывать он, и оба блока "fault" и "finally" получат параметры, указывающие, какое исключение (если оно есть) произошло во время выполнения попытки, и успешно ли завершение попытки (обратите внимание, кстати, что это возможно, что параметр исключения не равен NULL, даже если основная строка завершена; чистый код C# не может обнаружить такое условие, но обертка vb.net может).
Это будет ловить исключения, сгенерированные либо напрямую, либо внутри метода dispose:
try
{
using (MyWrapper wrapper = new MyWrapper())
{
throw new MyException("Bad error.");
}
}
catch ( MyException myex ) {
//deal with your exception
}
catch ( Exception ex ) {
//any other exception thrown by either
//MyWrapper..ctor() or MyWrapper.Dispose()
}
Но это зависит от них, использующих этот код - похоже, вы хотите, чтобы MyWrapper делал это вместо этого.
Оператор using просто для того, чтобы всегда вызывать Dispose. Это действительно делает это:
MyWrapper wrapper;
try
{
wrapper = new MyWrapper();
}
finally {
if( wrapper != null )
wrapper.Dispose();
}
Похоже, что вы хотите это:
MyWrapper wrapper;
try
{
wrapper = new MyWrapper();
}
finally {
try{
if( wrapper != null )
wrapper.Dispose();
}
catch {
//only errors thrown by disposal
}
}
Я бы посоветовал разобраться с этим в вашей реализации Dispose - вы все равно должны решать любые вопросы во время Dispose.
Если вы связываете какой-то ресурс, где вам нужно, чтобы пользователи вашего API каким-либо образом освободили его, подумайте о Close()
метод. Ваше распоряжение должно вызывать это тоже (если это еще не было), но пользователи вашего API могут также вызывать это сами, если им нужен более точный контроль.