Podczas przechodzenia na nową platformę .NET Core 3 IAsynsDisposable
natknąłem się na następujący problem.
Rdzeń problemu: jeśli DisposeAsync
zgłasza wyjątek, wyjątek ten ukrywa wszelkie wyjątki await using
zgł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 AsyncDispose
wyjątek, jeśli zostanie rzucony, a wyjątek od wewnątrz await using
tylko, jeśli AsyncDispose
nie zostanie rzucony .
Wolałbym jednak odwrotnie: uzyskać wyjątek od await using
bloku, jeśli to możliwe, i- DisposeAsync
wyjątek tylko wtedy, gdy await using
blok zakończy się pomyślnie.
Uzasadnienie: Wyobraź sobie, że moja klasa D
współpracuje z niektórymi zasobami sieciowymi i subskrybuje niektóre powiadomienia zdalnie. Kod w środku await using
moż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 DisposeAsync
jest odpowiedni. Oznacza to, że zniesienie wszystkich wyjątków w środku DisposeAsync
nie powinno być dobrym pomysłem.
Wiem, że ten sam problem występuje w przypadku niesynchronicznym: wyjątek w finally
zastę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 using
jeś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.
Dispose
zawsze brzmiało: „Mogło być nie tak: po prostu staraj się poprawić sytuację, ale nie pogarszaj”, i nie rozumiem, dlaczego AsyncDispose
powinno być inaczej.
DisposeAsync
moż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.
Close
metodę z tego właśnie powodu. Prawdopodobnie rozsądnie jest zrobić to samo:CloseAsync
próbuje ładnie zamknąć rzeczy i powoduje porażkę.DisposeAsync
po prostu daje z siebie wszystko i po cichu zawodzi.