Podczas przechodzenia na nową platformę .NET Core 3 IAsynsDisposablenatknąłem się na następujący problem.
Rdzeń problemu: jeśli DisposeAsynczgłasza wyjątek, wyjątek ten ukrywa wszelkie wyjątki await usingzgłoszone w bloku.
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");
}
}
Tym, co zostaje złapane, jest AsyncDisposewyjątek, jeśli zostanie rzucony, a wyjątek od wewnątrz await usingtylko, jeśli AsyncDisposenie zostanie rzucony .
Wolałbym jednak odwrotnie: uzyskać wyjątek od await usingbloku, jeśli to możliwe, i- DisposeAsyncwyjątek tylko wtedy, gdy await usingblok zakończy się pomyślnie.
Uzasadnienie: Wyobraź sobie, że moja klasa Dwspółpracuje z niektórymi zasobami sieciowymi i subskrybuje niektóre powiadomienia zdalnie. Kod w środku await usingmoże zrobić coś złego i zawieść kanał komunikacyjny, po czym kod w Dispose, który próbuje z wdziękiem zamknąć komunikację (np. Wypisać się z powiadomień), również się nie powiedzie. Ale pierwszy wyjątek daje mi prawdziwe informacje o problemie, a drugi to tylko problem wtórny.
W innym przypadku, gdy główna część przebiegła, a usuwanie nie powiodło się, prawdziwy problem jest w środku DisposeAsync, więc wyjątek od DisposeAsyncjest odpowiedni. Oznacza to, że zniesienie wszystkich wyjątków w środku DisposeAsyncnie powinno być dobrym pomysłem.
Wiem, że ten sam problem występuje w przypadku niesynchronicznym: wyjątek w finallyzastępuje wyjątek w try, dlatego nie zaleca się wrzucania Dispose(). Jednak w przypadku klas korzystających z dostępu do sieci tłumienie wyjątków w metodach zamykania wcale nie wygląda dobrze.
Możliwe jest obejście problemu za pomocą następującego pomocnika:
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 { }
}
}
}
i używaj go jak
await new D().UsingAsync(d =>
{
throw new ArgumentException("I'm inside using");
});
co jest trochę brzydkie (i uniemożliwia takie rzeczy, jak wczesne powroty do bloku używającego).
Czy istnieje dobre kanoniczne rozwiązanie, await usingjeśli to możliwe? Moje wyszukiwanie w Internecie nie znalazło nawet dyskusji na ten temat.
CloseAsyncśrodki, muszę podjąć dodatkowe środki ostrożności, aby uruchomić. Jeśli po prostu umieszczę go na końcu using-block, zostanie on pominięty przy wczesnych powrotach itp. (Tak chcielibyśmy, aby się wydarzyło) i wyjątkach (tak chcielibyśmy, aby tak się stało). Ale pomysł wygląda obiecująco.
Disposezawsze brzmiało: „Mogło być nie tak: po prostu staraj się poprawić sytuację, ale nie pogarszaj”, i nie rozumiem, dlaczego AsyncDisposepowinno być inaczej.
DisposeAsyncmożliwe, aby posprzątać, ale nie rzucać, jest właściwym rozwiązaniem. Miałeś na myśli celowe wczesne powroty, w których celowe wczesne powroty mogłyby omyłkowo ominąć wezwanie do CloseAsync: są to te, których zabrania wiele standardów kodowania.
Closemetodę z tego właśnie powodu. Prawdopodobnie rozsądnie jest zrobić to samo:CloseAsyncpróbuje ładnie zamknąć rzeczy i powoduje porażkę.DisposeAsyncpo prostu daje z siebie wszystko i po cichu zawodzi.