Определите, выполняется ли в блоке finally из-за генерируемого исключения
-
26-09-2020 - |
Вопрос
Можно ли определить, выполняется ли код в данный момент в контексте finally
обработчик в результате генерируемого исключения?Мне довольно нравится использовать IDisposable
шаблон для реализации функциональности определения области входа / выхода, но одна проблема с этим шаблоном заключается в том, что вы, возможно, не обязательно хотите, чтобы поведение конца области возникало, если в теле using
.Я бы искал что-то подобное этому:
public static class MyClass
{
public static void MyMethod()
{
using (var scope = MyScopedBehavior.Begin())
{
//Do stuff with scope here
}
}
}
public sealed class MyScopedBehavior : IDisposable
{
private MyScopedBehavior()
{
//Start of scope behavior
}
public void Dispose()
{
//I only want to execute the following if we're not unwinding
//through finally due to an exception:
//...End of scope behavior
}
public static MyScopedBehavior Begin()
{
return new MyScopedBehavior();
}
}
Есть другие способы, которыми я могу выполнить это (передать делегат функции, которая окружает вызов определенным поведением), но мне любопытно, возможно ли это сделать с помощью IDisposable
шаблон.
На самом деле, этот вопрос, по-видимому, уже задавался и на него давались ответы раньше здесь.Это можно обнаружить очень грубым способом.На самом деле я бы не стал использовать эту технику, но интересно знать, что это возможно.
Решение
Способы достижения этого, которые я видел, требуют дополнительного метода:
public static void MyMethod()
{
using (var scope = MyScopedBehavior.Begin())
{
//Do stuff with scope here
scope.Complete(); // Tells the scope that it's good
}
}
Делая это, ваш объект scope может отслеживать, удален ли он из-за ошибки или успешной операции.Это подход, принятый Транзакционный обзор, например (см. Обзор транзакций.Завершено).
Другие советы
В качестве боковой точки IL позволяет вам указать SEH fault
блоки , которые похожи на finally
но вводятся Только когда генерируется исключение - вы можете увидеть пример здесь, примерно на 2/3 страницы ниже по странице.К сожалению, C # не предоставляет эту функциональность.
Я искал что-то похожее для модульного тестирования - у меня есть вспомогательный класс, который я использую для очистки объектов после тестового запуска, и я хочу сохранить приятный, чистый синтаксис "using".Я также хотел иметь возможность не выполнять очистку в случае неудачи теста.То, что я придумал, - это позвонить Маршал.GetExceptionCode().Я не знаю, подходит ли это для всех случаев, но для тестового кода это, кажется, работает нормально.
Лучшее, что я могу придумать, было бы:
using (var scope = MyScopedBehavior.Begin())
{
try
{
//Do stuff with scope here
}
catch(Exception)
{
scope.Cancel();
throw;
}
}
Конечно, scope.Cancel()
убедился бы, что в Dispose() ничего не происходит
Следующий шаблон позволяет избежать проблемы с неправильным использованием API, т. е.метод завершения области, который не вызывается, т. е.полностью опущен или не вызывается из-за логического условия.Я думаю, что это более точно отвечает на ваш вопрос и требует еще меньше кода для пользователя API.
Редактировать
Еще более прямолинейно после комментария Дэна:
public class Bling
{
public static void DoBling()
{
MyScopedBehavior.Begin(() =>
{
//Do something.
}) ;
}
}
public static class MyScopedBehavior
{
public static void Begin(Action action)
{
try
{
action();
//Do additonal scoped stuff as there is no exception.
}
catch (Exception ex)
{
//Clean up...
throw;
}
}
}
Я думаю, что лучший способ - это использовать write out try/catch/finally
предложение вручную.Изучите пункт из первой книги "Эффективный c #".Хороший хакер C # должен точно знать, к чему приводит использование.Это немного изменилось со времен .Net 1.1 - теперь у вас может быть несколько, использующих одно под другим.Итак, используйте reflector и изучите код без сахара.
Затем, когда вы напишете свой собственный код - либо используйте using
или напиши свой собственный материал.Это не так уж трудно, и знать это полезно.
Вы могли бы пофантазировать с другими трюками, но они кажутся слишком тяжелыми и даже неэффективными.Позвольте мне включить пример кода.
ЛЕНИВЫЙ СПОСОБ:
using (SqlConnection cn = new SqlConnection(connectionString))
using (SqlCommand cm = new SqlCommand(commandString, cn))
{
cn.Open();
cm.ExecuteNonQuery();
}
РУЧНОЙ СПОСОБ:
bool sawMyEx = false;
SqlConnection cn = null;
SqlCommand cm = null;
try
{
cn = new SqlConnection(connectionString);
cm = new SqlCommand(commandString, cn);
cn.Open();
cm.ExecuteNonQuery();
}
catch (MyException myEx)
{
sawMyEx = true; // I better not tell my wife.
// Do some stuff here maybe?
}
finally
{
if (sawMyEx)
{
// Piss my pants.
}
if (null != cm);
{
cm.Dispose();
}
if (null != cn)
{
cn.Dispose();
}
}
Было бы (ИМХО очень) полезно, если бы существовал вариант IDisposable
чей Dispose
метод принял параметр, указывающий, какое исключение, если таковое имелось, находилось в ожидании при его запуске.Среди прочего, в том случае, если Dispose
не в состоянии выполнить ожидаемую очистку, он был бы в состоянии выдать исключение, которое включает информацию о более раннем исключении.Это также позволило бы Dispose
метод для генерирования исключения, если код "забывает" сделать что-то, что он должен был сделать в using
блокируйте, но не перезаписывайте любое другое исключение, которое может привести к преждевременному завершению использования блока.К сожалению, на данный момент такой функции не существует.
Существует множество статей, в которых предлагаются способы использования функций API для определения того, существует ли ожидающее исключение.Одна из основных проблем, связанных с такими подходами, заключается в том, что возможно, что код может выполняться в finally
блок для try
который успешно завершен, но который может быть вложен в finally
блок, чей try
вышел преждевременно.Даже если a Dispose
метод мог бы определить, что такая ситуация существует, у него не было бы способа узнать, какая try
заблокируйте, к которому он "принадлежал".Можно было бы сформулировать примеры, где применима любая ситуация.
Как бы то ни было, лучший подход, вероятно, состоит в том, чтобы иметь явный метод "success" и предполагать сбой, если он не вызывается, и считать, что последствия забывания вызвать метод "success" должны быть очевидны, даже если исключение не генерируется.Одна вещь, которая может быть полезна в качестве простого служебного метода, была бы чем-то вроде
T Success<T>(T returnValue)
{
Success();
return T;
}
таким образом, позволяя использовать такой код, как:
return scopeGuard.Success(thingThatMightThrow());
вместо того , чтобы
var result = thingThatMightThrow();
scopeGuard.Success();
return result;
Почему бы просто не избавиться изнутри try { }
блокировать в самом конце и вообще не использовать finally?Похоже, это именно то поведение, которое вы ищете.
Это также кажется более реалистичным с точки зрения того, как другие могут использовать ваш класс.Вы уверены, что все, кто когда-либо использует его, никогда не захотят утилизировать в случае исключения?Или это поведение должно обрабатываться потребителем класса?