Під час переходу на новий .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
просто робить все можливе і мовчить.