При переходе на новые .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просто делает все возможное, и молча терпит неудачу.