Beim Wechsel zu den neuen .NET Core 3 IAsynsDisposablebin ich auf das folgende Problem gestoßen.
Der Kern des Problems: Wenn DisposeAsynceine 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 AsyncDisposeAusnahme, wenn es geworfen wird, und die Ausnahme von innen await usingnur, wenn AsyncDisposees nicht wirft.
Ich würde es jedoch andersherum vorziehen: await usingwenn möglich die Ausnahme vom Block zu bekommen und DisposeAsync-ausnahme nur, wenn der await usingBlock erfolgreich beendet wurde.
Begründung: Stellen Sie sich vor, meine Klasse Darbeitet mit einigen Netzwerkressourcen und abonniert einige Remote-Benachrichtigungen. Der darin enthaltene Code await usingkann 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 DisposeAsyncder relevanten ist. Dies bedeutet, dass es DisposeAsynckeine 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 usingwenn möglich? Bei meiner Suche im Internet wurde dieses Problem nicht einmal diskutiert.
CloseAsyncMittel 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.
Disposewar 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 AsyncDisposees anders sein sollte.
DisposeAsyncBeste 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.
Closeaus diesem Grund eine separate Methode. Es ist wahrscheinlich ratsam, dasselbe zu tun:CloseAsyncVersuche, die Dinge gut zu schließen, und werfen einen Fehler auf.DisposeAsynctut einfach sein Bestes und versagt lautlos.