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.
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.CloseAsync
Mittel bedeutet, dass ich zusätzliche Vorsichtsmaßnahmen treffen muss, um es zum Laufen zu bringen. Wenn ich es nur am Ende vonusing
-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, warumAsyncDispose
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önnteCloseAsync
: Dies sind diejenigen, die von vielen Codierungsstandards verboten sind.Antworten:
Es gibt Ausnahmen, die auftauchen sollen (die aktuelle Anforderung unterbrechen oder den Prozess beenden), und es gibt Ausnahmen, von denen Ihr Entwurf erwartet, dass sie manchmal auftreten und die Sie behandeln können (z. B. erneut versuchen und fortfahren).
Die Unterscheidung zwischen diesen beiden Typen liegt jedoch beim endgültigen Aufrufer des Codes - dies ist der springende Punkt bei Ausnahmen, um die Entscheidung dem Anrufer zu überlassen.
Manchmal legt der Anrufer größeren Wert darauf, dass die Ausnahme aus dem ursprünglichen Codeblock und manchmal die Ausnahme aus dem ursprünglichen Codeblock angezeigt wird
Dispose
. Es gibt keine allgemeine Regel für die Entscheidung, welche Priorität haben soll. Die CLR ist mindestens konsistent (wie Sie bemerkt haben) zwischen dem Synchronisierungs- und dem nicht asynchronen Verhalten.Es ist vielleicht bedauerlich, dass wir jetzt
AggregateException
mehrere Ausnahmen darstellen müssen. Es kann nicht nachgerüstet werden, um dies zu lösen. dh wenn eine Ausnahme bereits im Flug ist und eine andere ausgelöst wird, werden sie zu einer zusammengefasstAggregateException
. Dercatch
Mechanismus kann so geändert werden, dass beim Schreiben alle Mechanismencatch (MyException)
abgefangen werdenAggregateException
, die eine Ausnahme vom Typ enthaltenMyException
. Es gibt jedoch verschiedene andere Komplikationen, die sich aus dieser Idee ergeben, und es ist wahrscheinlich zu riskant, etwas so Grundlegendes jetzt zu modifizieren.Sie könnten Ihre verbessern
UsingAsync
, um die frühzeitige Rückgabe eines Wertes zu unterstützen:quelle
await using
? Ihre Idee ist, dass in einigen Fällen nur Standard verwendet werden kann (hier wirft DisposeAsync keinen nicht tödlichen Fall), und ein Helfer wieUsingAsync
ist besser geeignet (wenn DisposeAsync wahrscheinlich wirft). ? (Natürlich müsste ich modifizierenUsingAsync
, damit nicht alles blind erfasst wird, sondern nur nicht tödlich (und in Eric Lipperts Gebrauch nicht ohne Kopf ).)Vielleicht verstehen Sie bereits, warum dies passiert, aber es lohnt sich, dies zu formulieren. Dieses Verhalten ist nicht spezifisch für
await using
. Es würde auch mit einem einfachenusing
Block passieren . Während ichDispose()
hier sage , gilt das alles auch fürDisposeAsync()
.Ein
using
Block ist nur syntaktischer Zucker für einentry
/finally
Block, wie im Abschnitt "Anmerkungen" der Dokumentation angegeben . Was Sie sehen , geschieht , weil derfinally
Block immer läuft, auch nach einer Ausnahme. Wenn also eine Ausnahme auftritt und keincatch
Block vorhanden ist , wird die Ausnahme angehalten, bis derfinally
Block ausgeführt wird, und dann wird die Ausnahme ausgelöst. Wenn jedoch eine Ausnahme auftrittfinally
, wird die alte Ausnahme nie angezeigt.Sie können dies anhand dieses Beispiels sehen:
Es spielt keine Rolle, ob
Dispose()
oderDisposeAsync()
innerhalb der aufgerufen wirdfinally
. Das Verhalten ist das gleiche.Mein erster Gedanke ist: nicht einwerfen
Dispose()
. Aber nachdem ich einige von Microsofts eigenen Codes überprüft habe, denke ich, dass es davon abhängt.Schauen Sie sich
FileStream
zum Beispiel deren Implementierung an . Sowohl die synchroneDispose()
Methode als auchDisposeAsync()
können tatsächlich Ausnahmen auslösen. Die synchroneDispose()
tut ignorieren einige Ausnahmen absichtlich, aber nicht alle.Aber ich denke, es ist wichtig, die Natur Ihrer Klasse zu berücksichtigen. In a
FileStream
wird beispielsweiseDispose()
der Puffer in das Dateisystem geleert. Das ist eine sehr wichtige Aufgabe und Sie müssen wissen, ob dies fehlgeschlagen ist . Das kann man nicht einfach ignorieren.Bei anderen
Dispose()
Objekttypen haben Sie beim Aufrufen jedoch wirklich keine Verwendung mehr für das Objekt. AnrufenDispose()
bedeutet wirklich nur "dieses Objekt ist für mich tot". Möglicherweise wird der zugewiesene Speicher bereinigt, aber ein Fehler hat keinerlei Auswirkungen auf den Betrieb Ihrer Anwendung. In diesem Fall können Sie die Ausnahme in Ihrem ignorierenDispose()
.Wenn Sie jedoch in jedem Fall zwischen einer Ausnahme innerhalb der
using
oder einer Ausnahme, von der sie stammtDispose()
, unterscheiden möchten, benötigen Sie einentry
/catch
-Block sowohl innerhalb als auch außerhalb Ihresusing
Blocks:Oder du könntest es einfach nicht benutzen
using
. Formulieren Sie eintry
/catch
/finally
Block selbst, wo Sie jede Ausnahme fangen infinally
:quelle
catch
innerhalb desusing
Blocks würde nicht helfen, da die Ausnahmebehandlung normalerweise irgendwo weit vomusing
Block selbst entfernt erfolgt, so dass die Behandlung innerhalb des Blocksusing
normalerweise nicht sehr praktikabel ist. Über die Verwendung von Neinusing
- ist es wirklich besser als die vorgeschlagene Problemumgehung?try
/catch
/finally
block zu sehen, da sofort klar wäre, was es tut, ohne lesen zu müssen, wasAsyncUsing
es tut. Sie behalten auch die Möglichkeit einer vorzeitigen Rückgabe. Es fallen auch zusätzliche CPU-Kosten für Sie anAwaitUsing
. Es wäre klein, aber es ist da.Dispose()
nicht geworfen werden sollte, da es mehr als einmal aufgerufen wird. Microsofts eigene Implementierungen können aus gutem Grund Ausnahmen auslösen, wie ich in dieser Antwort gezeigt habe. Ich stimme jedoch zu, dass Sie es vermeiden sollten, wenn es überhaupt möglich ist, da normalerweise niemand erwarten würde, dass es wirft.using ist effektiv Exception Handling Code (Syntax Zucker für try ... finally ... Dispose ()).
Wenn Ihr Code für die Ausnahmebehandlung Ausnahmen auslöst, wird etwas auf königliche Weise zerstört.
Was auch immer passiert ist, um dich dorthin zu bringen, spielt keine Rolle mehr. Fehlerhafter Ausnahmebehandlungscode verbirgt alle möglichen Ausnahmen auf die eine oder andere Weise. Der Ausnahmebehandlungscode muss festgelegt sein, der absolute Priorität hat. Ohne das erhalten Sie nie genug Debugging-Daten für das eigentliche Problem. Ich sehe es extrem oft falsch gemacht. Es ist ungefähr so leicht, sich zu irren, wie mit nackten Zeigern umzugehen. So oft gibt es zwei Artikel zu dem von mir verlinkten Thema, die Ihnen bei allen zugrunde liegenden Designfehlern helfen könnten:
Abhängig von der Ausnahmeklassifizierung müssen Sie Folgendes tun, wenn Ihr Ausnahmebehandlungs- / Dipose-Code eine Ausnahme auslöst:
Für Fatal, Boneheaded und Vexing ist die Lösung dieselbe.
Exogene Ausnahmen müssen auch bei schwerwiegenden Kosten vermieden werden. Es gibt einen Grund, warum wir immer noch Protokolldateien verwenden, anstatt Datenbanken zu protokollieren, um Ausnahmen zu protokollieren. DB Opeartions sind nur eine Möglichkeit, auf exogene Probleme zu stoßen. Protokolldateien sind der einzige Fall, in dem es mir nichts ausmacht, wenn Sie das Dateihandle während der gesamten Laufzeit geöffnet lassen.
Wenn Sie eine Verbindung schließen müssen, sorgen Sie sich nicht zu sehr um das andere Ende. Behandeln Sie es wie UDP: "Ich werde die Informationen senden, aber es ist mir egal, ob die andere Seite es bekommt." Bei der Entsorgung geht es darum, Ressourcen auf der Client-Seite / Seite, an der Sie arbeiten, zu bereinigen.
Ich kann versuchen, sie zu benachrichtigen. Aber Sachen auf der Server / FS-Seite aufräumen? Dafür sind ihre Timeouts und ihre Ausnahmebehandlung verantwortlich.
quelle
NativeMethods.UnmapViewOfFile();
und könnte noch etwas passierenNativeMethods.CloseHandle()
. Aber diese werden von extern importiert. Es gibt keine Überprüfung von Rückgabewerten oder anderen Elementen, die verwendet werden könnten, um eine ordnungsgemäße .NET-Ausnahme für alle diese beiden Probleme zu erhalten. Ich gehe also stark davon aus, dass es SqlConnection.Dispose (bool) einfach egal ist. | Close ist viel schöner, wenn man es dem Server sagt. Bevor es anruft entsorgen.Sie können versuchen, AggregateException zu verwenden und Ihren Code folgendermaßen zu ändern:
https://docs.microsoft.com/ru-ru/dotnet/api/system.aggregateexception?view=netframework-4.8
https://docs.microsoft.com/ru-ru/dotnet/standard/parallel-programming/exception-handling-task-parallel-library
quelle