Beim Wechsel zu den neuen .NET Core 3 IAsynsDisposable
bin ich auf das folgende Problem gestoßen.
Der Kern des Problems: Wenn DisposeAsync
eine Ausnahme ausgelöst wird, werden durch diese Ausnahme alle innerhalb von await using
-block ausgelösten Ausnahmen ausgeblendet.
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");
}
}
Was gefangen wird, ist die AsyncDispose
Ausnahme, wenn es geworfen wird, und die Ausnahme von innen await using
nur, wenn AsyncDispose
es nicht wirft.
Ich würde es jedoch andersherum vorziehen: await using
wenn möglich die Ausnahme vom Block zu bekommen und DisposeAsync
-ausnahme nur, wenn der await using
Block erfolgreich beendet wurde.
Begründung: Stellen Sie sich vor, meine Klasse D
arbeitet mit einigen Netzwerkressourcen und abonniert einige Remote-Benachrichtigungen. Der darin enthaltene Code await using
kann etwas falsch machen und den Kommunikationskanal ausfallen lassen. Danach schlägt auch der Code in Dispose fehl, der versucht, die Kommunikation ordnungsgemäß zu schließen (z. B. das Abbestellen der Benachrichtigungen). Aber die erste Ausnahme gibt mir die wirklichen Informationen über das Problem, und die zweite ist nur ein sekundäres Problem.
In dem anderen Fall, in dem der Hauptteil durchlaufen wurde und die Entsorgung fehlgeschlagen ist, liegt das eigentliche Problem im Inneren DisposeAsync
, sodass die Ausnahme von DisposeAsync
der relevanten ist. Dies bedeutet, dass es DisposeAsync
keine gute Idee sein sollte, alle darin enthaltenen Ausnahmen zu unterdrücken .
Ich weiß, dass es das gleiche Problem mit nicht asynchronen Fällen gibt: Ausnahme in finally
überschreibt die Ausnahme in try
, deshalb wird nicht empfohlen, sie einzufügen Dispose()
. Bei Klassen mit Netzwerkzugriff sieht das Unterdrücken von Ausnahmen beim Schließen von Methoden jedoch überhaupt nicht gut aus.
Es ist möglich, das Problem mit dem folgenden Helfer zu umgehen:
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 { }
}
}
}
und benutze es gerne
await new D().UsingAsync(d =>
{
throw new ArgumentException("I'm inside using");
});
Das ist irgendwie hässlich (und verbietet Dinge wie frühe Rückkehr innerhalb des using-Blocks).
Gibt es eine gute kanonische Lösung, await using
wenn möglich? Bei meiner Suche im Internet wurde dieses Problem nicht einmal diskutiert.
CloseAsync
Mittel bedeutet, dass ich zusätzliche Vorsichtsmaßnahmen treffen muss, um es zum Laufen zu bringen. Wenn ich es nur am Ende von using
-block setze, wird es bei vorzeitiger Rückgabe usw. (das ist, was wir wollen) und Ausnahmen (das ist, was wir wollen) übersprungen. Aber die Idee sieht vielversprechend aus.
Dispose
war schon immer "Dinge könnten schief gelaufen sein: Gib einfach dein Bestes, um die Situation zu verbessern, aber mach es nicht schlimmer", und ich verstehe nicht, warum AsyncDispose
es anders sein sollte.
DisposeAsync
Beste zu tun, um aufzuräumen, aber nicht zu werfen . Sie sprachen von absichtlichen vorzeitigen Rückgaben, bei denen eine absichtliche vorzeitige Rückgabe fälschlicherweise einen Anruf an umgehen könnte CloseAsync
: Dies sind diejenigen, die von vielen Codierungsstandards verboten sind.
Close
aus diesem Grund eine separate Methode. Es ist wahrscheinlich ratsam, dasselbe zu tun:CloseAsync
Versuche, die Dinge gut zu schließen, und werfen einen Fehler auf.DisposeAsync
tut einfach sein Bestes und versagt lautlos.