Під час переходу на новий .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вилучення, якщо воно кинене, і виняток зсередини, await usingлише якщо AsyncDisposeвін не кидає.
Однак я б вважав за краще навпаки: отримати виняток із await usingблоку, якщо це можливо, та DisposeAsync-виключити, лише якщо 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");
});
який є некрасивим (і забороняє такі речі, як раннє повернення всередину використовуючого блоку).
Чи є добре, канонічне рішення, await usingякщо це можливо? Мій пошук в Інтернеті не знайшов навіть обговорення цієї проблеми.
CloseAsyncзасіб, що мені потрібно вжити додаткових запобіжних заходів для його запуску. Якщо я просто покладу це в кінці using-блока, він буде пропущений на ранньому поверненні тощо (це те, що ми хотіли б, щоб це сталося) та винятки (це те, що ми хотіли б, щоб це сталося). Але ідея виглядає багатообіцяючо.
Disposeзавжди було: "Можливо, все пішло не так: просто зробіть все можливе, щоб покращити ситуацію, але не погіршуйте її", і я не бачу, чому AsyncDisposeслід інакше.
DisposeAsyncзробити все можливе, щоб привести в порядок, але не кидати - це правильно зробити. Ви говорили про навмисне раннє повернення, де навмисне раннє повернення може помилково обійти виклик CloseAsync: саме ті заборонені багатьма стандартами кодування.
Closeцю методику. Напевно, розумно робити те саме:CloseAsyncспроби красиво закрити речі та кидає невдачу.DisposeAsyncпросто робить все можливе і мовчить.