При переходе на новые .NET Core 3 IAsynsDisposable
я наткнулся на следующую проблему.
Суть проблемы: если DisposeAsync
выбрасывает исключение, это исключение скрывает любые исключения, созданные внутри await using
-блока.
class Program
{
static async Task Main()
{
try
{
await using (var d = new D())
{
throw new ArgumentException("I'm inside using");
}
}
catch (Exception e)
{
Console.WriteLine(e.Message); // prints I'm inside dispose
}
}
}
class D : IAsyncDisposable
{
public async ValueTask DisposeAsync()
{
await Task.Delay(1);
throw new Exception("I'm inside dispose");
}
}
Что ловится, так это AsyncDispose
-exception, если он брошен, и исключение изнутри, await using
только если AsyncDispose
не бросает.
Однако я предпочел бы это наоборот: получить исключение из await using
блока, если это возможно, и DisposeAsync
-exception, только если await using
блок завершился успешно.
Обоснование: представьте, что мой класс D
работает с некоторыми сетевыми ресурсами и подписывается на некоторые удаленные уведомления. Код внутри await using
может сделать что-то не так и выйти из строя канала связи, после чего код в Dispose, который пытается изящно закрыть сообщение (например, отписаться от уведомлений), тоже потерпит неудачу. Но первое исключение дает мне реальную информацию о проблеме, а второе - просто вторичная проблема.
В другом случае, когда основная часть прошла, а утилизация не удалась, реальная проблема - внутри DisposeAsync
, поэтому исключение DisposeAsync
- релевантное. Это означает, что просто подавление всех исключений внутри DisposeAsync
не должно быть хорошей идеей.
Я знаю, что с не асинхронным случаем возникает та же проблема: исключение в finally
переопределяет исключение в try
, поэтому не рекомендуется добавлять его Dispose()
. Но с классами доступа к сети, подавляющими исключения в методах закрытия, все выглядит не очень хорошо.
Можно обойти проблему с помощью следующего помощника:
static class AsyncTools
{
public static async Task UsingAsync<T>(this T disposable, Func<T, Task> task)
where T : IAsyncDisposable
{
bool trySucceeded = false;
try
{
await task(disposable);
trySucceeded = true;
}
finally
{
if (trySucceeded)
await disposable.DisposeAsync();
else // must suppress exceptions
try { await disposable.DisposeAsync(); } catch { }
}
}
}
и использовать его как
await new D().UsingAsync(d =>
{
throw new ArgumentException("I'm inside using");
});
что довольно уродливо (и запрещает такие вещи, как досрочный возврат в блоке using).
Есть ли хорошее, каноническое решение, await using
если возможно? Мой поиск в интернете не нашел даже обсуждения этой проблемы.
Close
метод именно по этой причине. Вероятно, разумно сделать то же самое:CloseAsync
пытаться красиво закрыть вещи и бросить на неудачу.DisposeAsync
просто делает все возможное, и молча терпит неудачу.CloseAsync
средство, мне нужно принять дополнительные меры предосторожности, чтобы запустить его. Если я просто поставлю его в концеusing
-блока, он будет пропущен при досрочном возврате и т. Д. (Это то, что мы хотели бы случиться) и исключениях (это то, что мы хотели бы случиться). Но идея выглядит многообещающей.Dispose
всегда было «Все могло пойти не так: просто приложите все усилия, чтобы улучшить ситуацию, но не усугубляйте ситуацию», и я не понимаю, почемуAsyncDispose
должно быть иначе.DisposeAsync
все возможное, чтобы привести в порядок, а не бросить, - это правильно. Вы говорили о преднамеренном досрочном возврате, когда преднамеренное досрочное возвращение может ошибочно обойти вызовCloseAsync
: это те, которые запрещены многими стандартами кодирования.Ответы:
Существуют исключения, которые вы хотите отобразить (прервать текущий запрос или остановить процесс), и есть исключения, которые, как ожидает ваш проект, иногда будут возникать, и вы можете их обработать (например, повторить попытку и продолжить).
Но различие между этими двумя типами зависит от конечного пользователя кода - в этом и заключается весь смысл исключений, чтобы решение оставлялось за пользователем.
Иногда вызывающий объект будет уделять больше внимания появлению исключения из исходного блока кода, а иногда исключения из
Dispose
. Не существует общего правила для принятия решения, какой из них должен иметь приоритет. CLR, по крайней мере, соответствует (как вы заметили) между синхронизацией и не асинхронным поведением.Возможно, очень жаль, что теперь мы должны
AggregateException
представлять несколько исключений, но это не может быть модифицировано для решения этой проблемы. то есть, если исключение уже находится в полете, а другое выброшено, они объединяются вAggregateException
.catch
Механизм может быть изменен таким образом , что если вы пишетеcatch (MyException)
то поймает ,AggregateException
что включает в себя исключение типаMyException
. Есть и другие сложности, вытекающие из этой идеи, и, вероятно, слишком рискованно изменять что-то столь фундаментальное сейчас.Вы можете улучшить свою работу,
UsingAsync
чтобы обеспечить ранний возврат значения:источник
await using
может использоваться только стандартный (это где DisposeAsync не будет генерировать в нефатальном случае), и вспомогательныйUsingAsync
тип больше подходит (если DisposeAsync, скорее всего, выбрасывает) ? (Конечно, мне нужно изменить,UsingAsync
чтобы он не ловил все вслепую, а только не смертельно (и не без умолку в использовании Эрика Липперта ).)Возможно, вы уже понимаете, почему это происходит, но это стоит объяснить. Это поведение не является специфическим для
await using
. Это может произойти и с простымusing
блоком. Так что, хотя я и говорюDispose()
здесь, все это относится и кDisposeAsync()
.using
Блок просто синтаксический сахар дляtry
/finally
блока, как раздел замечания документации говорит. То, что вы видите, происходит потому, чтоfinally
блок всегда выполняется, даже после исключения. Таким образом, если происходит исключение, иcatch
блока нет , исключение переводится в режим ожидания до тех пор, покаfinally
блок не будет запущен, а затем выбрасывается исключение. Но если произойдет исключениеfinally
, вы никогда не увидите старое исключение.Вы можете увидеть это на следующем примере:
Это не имеет значения,
Dispose()
илиDisposeAsync()
вызывается внутриfinally
. Поведение такое же.Моя первая мысль: не бросайся
Dispose()
. Но после просмотра некоторого собственного кода Microsoft, я думаю, что это зависит.Взгляните на их реализацию
FileStream
, например. И синхронныйDispose()
метод, и наDisposeAsync()
самом деле может генерировать исключения. СинхронныйDispose()
действительно игнорирует некоторые исключения намеренно, но не все.Но я думаю, что важно принимать во внимание характер вашего класса. В
FileStream
, например,Dispose()
будет сброшен буфер в файловую систему. Это очень важная задача, и вам нужно знать, если это не удалось . Вы не можете просто игнорировать это.Однако в других типах объектов, когда вы вызываете
Dispose()
, вы действительно больше не используете этот объект. ВызовDispose()
действительно означает «этот объект мертв для меня». Возможно, он очищает часть выделенной памяти, но сбой никак не влияет на работу вашего приложения. В этом случае вы можете проигнорировать исключение внутри вашегоDispose()
.Но в любом случае, если вы хотите различить исключение внутри
using
или от исключенияDispose()
, то вам нужен блокtry
/catch
внутри и снаружи вашегоusing
блока:Или вы можете просто не использовать
using
. Запишитеtry
/catch
/finally
заблокируйте себя, где вы поймаете любое исключение вfinally
:источник
catch
Внутри вusing
блоке будет не поможет , потому что , как правило , обработка исключений делается где - то далеко отusing
самого блока, так его обработки внутриusing
, как правило , не очень возможно. Об использовании нетusing
- действительно ли это лучше, чем предлагаемый обходной путь?try
/catch
/finally
блок, так как было бы сразу понятно, что он делает, не читая, чтоAsyncUsing
делает. Вы также сохраняете возможность сделать ранний возврат. Там будет также дополнительная стоимость процессора для вашегоAwaitUsing
. Это было бы мало, но это есть.Dispose()
не должен бросать, потому что он вызывается более одного раза. Собственные реализации Microsoft могут генерировать исключения, и на то есть веские причины, как я показал в этом ответе. Тем не менее, я согласен с тем, что вам следует избегать этого, если это вообще возможно, поскольку никто обычно не ожидает, что он бросит.эффективно использовать код обработки исключений (синтаксический сахар для try ... finally ... Dispose ()).
Если ваш код обработки исключений генерирует исключения, что-то по-королевски сбито.
Что бы ни случилось, чтобы даже попасть туда, больше не имеет значения. Код обработки неисправных исключений будет скрывать все возможные исключения, так или иначе. Код обработки исключений должен быть фиксированным, который имеет абсолютный приоритет. Без этого вы никогда не получите достаточно данных отладки для реальной проблемы. Я часто вижу, что это неправильно. Это так же легко ошибиться, как обращение с голыми указателями. Очень часто есть две статьи по тематической ссылке, которые могут помочь вам с любыми неверными представлениями о дизайне:
В зависимости от классификации исключений, это то, что вам нужно сделать, если ваш код обработки исключений / дипозе выдает исключение:
Для Fatal, Boneheaded и Vexing решение одинаково.
Экзогенных исключений следует избегать даже при серьезных затратах. Есть причина, по которой мы все еще используем файлы журналов, а не базы данных журналов для регистрации исключений - операции с БД - это просто способ столкнуться с экзогенными проблемами. Лог-файлы - это тот случай, когда я даже не возражаю, если вы сохраните дескриптор файла открытым во время выполнения.
Если вам нужно закрыть соединение, не беспокойтесь о другом конце. Обрабатывайте это так, как это делает UDP: «Я отправлю информацию, но мне все равно, получит ли это другая сторона». Утилизация - это очистка ресурсов на стороне клиента / стороне, над которой вы работаете.
Я могу попытаться уведомить их. Но убирать вещи на стороне сервера / FS? То есть то , что их тайм - ауты и их обработка исключений несет ответственность.
источник
NativeMethods.UnmapViewOfFile();
иNativeMethods.CloseHandle()
. Но те импортированы из внешнего. Нет проверки какого-либо возвращаемого значения или чего-либо еще, что может быть использовано для получения правильного исключения .NET вокруг того, с чем эти два могут столкнуться. Так что я сильно сомневаюсь, SqlConnection.Dispose (bool) просто не волнует. | Закрыть гораздо приятнее, на самом деле это говорит сервер. Перед тем как звонить утилизировать.Вы можете попробовать использовать AggregateException и изменить свой код примерно так:
https://docs.microsoft.com/ru-ru/dotnet/api/system.aggregateexception?view=netframework-4.8
https://docs.microsoft.com/ru-ru/dotnet/standard/parallel-programming/exception-handling-task-parallel-library
источник