При переходе на новые .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
если возможно? Мой поиск в интернете не нашел даже обсуждения этой проблемы.
CloseAsync
средство, мне нужно принять дополнительные меры предосторожности, чтобы запустить его. Если я просто поставлю его в конце using
-блока, он будет пропущен при досрочном возврате и т. Д. (Это то, что мы хотели бы случиться) и исключениях (это то, что мы хотели бы случиться). Но идея выглядит многообещающей.
Dispose
всегда было «Все могло пойти не так: просто приложите все усилия, чтобы улучшить ситуацию, но не усугубляйте ситуацию», и я не понимаю, почему AsyncDispose
должно быть иначе.
DisposeAsync
все возможное, чтобы привести в порядок, а не бросить, - это правильно. Вы говорили о преднамеренном досрочном возврате, когда преднамеренное досрочное возвращение может ошибочно обойти вызов CloseAsync
: это те, которые запрещены многими стандартами кодирования.
Close
метод именно по этой причине. Вероятно, разумно сделать то же самое:CloseAsync
пытаться красиво закрыть вещи и бросить на неудачу.DisposeAsync
просто делает все возможное, и молча терпит неудачу.