JavaScript-Ressourcenmanagement
Dieser Leitfaden behandelt, wie man in JavaScript Ressourcenmanagement betreibt. Ressourcenmanagement ist nicht genau dasselbe wie das Speicherverwaltung, ein fortgeschritteneres Thema, das normalerweise automatisch von JavaScript gehandhabt wird. Ressourcenmanagement bezieht sich auf die Verwaltung von Ressourcen, die nicht automatisch von JavaScript aufgeräumt werden. Manchmal ist es in Ordnung, einige ungenutzte Objekte im Speicher zu haben, da sie nicht die Anwendungslogik stören, aber Ressourcenausfälle führen oft dazu, dass Dinge nicht funktionieren oder eine Menge überschüssiger Speicherplatz genutzt wird. Daher ist dies keine optionale Eigenschaft zur Optimierung, sondern eine Kernfunktion zum Schreiben korrekter Programme!
Hinweis:
Während Speicherverwaltung und Ressourcenmanagement zwei separate Themen sind, können Sie manchmal auf das System der Speicherverwaltung zugreifen, um Ressourcen zu verwalten, als letzten Ausweg. Wenn Sie beispielsweise ein JavaScript-Objekt haben, das einen Handle einer externen Ressource darstellt, können Sie ein FinalizationRegistry
erstellen, um die Ressource zu bereinigen, wenn der Handle vom Garbage Collector entfernt wird, da es definitiv keinen Weg gibt, danach auf die Ressource zuzugreifen. Es gibt jedoch keine Garantie dafür, dass der Finalizer ausgeführt wird, daher ist es keine gute Idee, sich für kritische Ressourcen darauf zu verlassen.
Problem
Lassen Sie uns zunächst einige Beispiele von Ressourcen betrachten, die verwaltet werden müssen:
-
Datei-Handles: Ein Datei-Handle wird verwendet, um Bytes in einer Datei zu lesen und zu schreiben. Wenn Sie damit fertig sind, müssen Sie
fileHandle.close()
aufrufen, andernfalls bleibt die Datei offen, selbst wenn das JS-Objekt nicht mehr zugänglich ist. Wie in den verlinkten Node.js-Dokumenten gesagt wird:Wenn ein
<FileHandle>
nicht mit derfileHandle.close()
-Methode geschlossen wird, versucht es, den Dateideskriptor automatisch zu schließen und eine Prozesswarnung auszugeben, um Speicherlecks zu verhindern. Bitte verlassen Sie sich nicht auf dieses Verhalten, da es unzuverlässig sein kann und die Datei möglicherweise nicht geschlossen wird. Stattdessen sollten Sie<FileHandle>
s immer explizit schließen. Node.js kann dieses Verhalten in Zukunft ändern. -
Netzwerkverbindungen: Einige Verbindungen, wie
WebSocket
undRTCPeerConnection
, müssen geschlossen werden, wenn keine Nachrichten übertragen werden. Andernfalls bleibt die Verbindung offen, und Verbindungspools sind oft sehr begrenzt in ihrer Größe. -
Stream-Leser: Wenn Sie nicht
ReadableStreamDefaultReader.releaseLock()
aufrufen, wird der Stream gesperrt und erlaubt keinem anderen Leser, ihn zu konsumieren.
Hier ist ein konkretes Beispiel, bei dem ein lesbarer Stream verwendet wird:
const stream = new ReadableStream({
start(controller) {
controller.enqueue("a");
controller.enqueue("b");
controller.enqueue("c");
controller.close();
},
});
async function readUntil(stream, text) {
const reader = stream.getReader();
let chunk = await reader.read();
while (!chunk.done && chunk.value !== text) {
console.log(chunk);
chunk = await reader.read();
}
// We forgot to release the lock here
}
readUntil(stream, "b").then(() => {
const anotherReader = stream.getReader();
// TypeError: ReadableStreamDefaultReader constructor can only
// accept readable streams that are not yet locked to a reader
});
Hier haben wir einen Stream, der drei Datenblöcke übernimmt. Wir lesen vom Stream, bis wir den Buchstaben "b" finden. Wenn readUntil
zurückkehrt, ist der Stream nur teilweise gelesen, sodass wir in der Lage sein sollten, mit einem anderen Leser weiter zu lesen. Wir haben jedoch vergessen, das Lock zu lösen, sodass der Stream immer noch gesperrt ist und kein weiterer Leser erstellt werden kann.
Die Lösung in diesem Fall ist einfach: Rufen Sie reader.releaseLock()
am Ende von readUntil
auf. Aber einige Probleme bleiben bestehen:
-
Inkonsistenz: Verschiedene Ressourcen haben unterschiedliche Wege, freigegeben zu werden. Zum Beispiel haben wir
close()
,releaseLock()
,disconnect()
, etc. Das Muster verallgemeinert sich nicht. -
Fehlerbehandlung: Was passiert, wenn der Aufruf von
reader.read()
fehlschlägt? Dann würdereadUntil
beendet und nie zum Aufruf vonreader.releaseLock()
gelangen. Wir können dies beheben, indem wirtry...finally
verwenden:jsasync function readUntil(stream, text) { const reader = stream.getReader(); try { let chunk = await reader.read(); while (!chunk.done && chunk.value !== text) { console.log(chunk); chunk = await reader.read(); } } finally { reader.releaseLock(); } }
Aber Sie müssen sich daran erinnern, dies jedes Mal zu tun, wenn Sie eine wichtige Ressource freigeben müssen.
-
Scoping: Im obigen Beispiel ist
reader
bereits geschlossen, wenn wir dietry...finally
-Anweisung verlassen, aber es bleibt in seinem Gültigkeitsbereich verfügbar. Dies bedeutet, dass Sie es versehentlich nach dem Schließen verwenden können. -
Mehrere Ressourcen: Wenn wir zwei Leser auf verschiedenen Streams haben, müssen wir daran denken, beide freizugeben. Dies ist ein respektabler Versuch, dies zu tun:
jsconst reader1 = stream1.getReader(); const reader2 = stream2.getReader(); try { // do something with reader1 and reader2 } finally { reader1.releaseLock(); reader2.releaseLock(); }
Dies führt jedoch zu mehr Fehlerbehandlungsproblemen. Wenn
stream2.getReader()
einen Fehler auslöst, wirdreader1
nicht freigegeben; wennreader1.releaseLock()
einen Fehler auslöst, wirdreader2
nicht freigegeben. Dies bedeutet, dass wir tatsächlich jedes Ressourcen-Akquisitions- und -Freigabepaar in seinem eigenentry...finally
umwickeln müssen:jsconst reader1 = stream1.getReader(); try { const reader2 = stream2.getReader(); try { // do something with reader1 and reader2 } finally { reader2.releaseLock(); } } finally { reader1.releaseLock(); }
Sie sehen, wie eine scheinbar harmlose Aufgabe des Aufrufens von releaseLock
schnell zu einem verschachtelten Boilerplate-Code führen kann. Deshalb bietet JavaScript integrierte Sprachunterstützung für das Ressourcenmanagement.
Die using
- und await using
-Deklarationen
Die Lösung, die wir haben, sind zwei spezielle Arten der Variablendeklaration: using
und await using
. Sie sind const
ähnlich, aber sie geben die Ressource automatisch frei, wenn die Variable aus dem Gültigkeitsbereich austritt, solange die Ressource freigebbar ist. Am gleichen Beispiel wie oben können wir es umschreiben als:
{
using reader1 = stream1.getReader();
using reader2 = stream2.getReader();
// do something with reader1 and reader2
// Before we exit the block, reader1 and reader2 are automatically released
}
Hinweis:
Zum Zeitpunkt des Schreibens implementiert ReadableStreamDefaultReader
das disposable-Protokoll nicht. Dies ist ein hypothetisches Beispiel.
Beachten Sie zuerst die zusätzlichen geschwungenen Klammern um den Code. Dies erzeugt einen neuen Blockbereich für die using
-Deklarationen. Mit using
deklarierte Ressourcen werden automatisch freigegeben, wenn sie aus dem Gültigkeitsbereich von using
austreten, was in diesem Fall der Fall ist, wenn wir den Block verlassen, entweder weil alle Anweisungen ausgeführt wurden oder weil irgendwo ein Fehler, ein return
/break
/continue
aufgetreten ist.
Dies bedeutet, dass using
nur in einem Gültigkeitsbereich verwendet werden kann, der eine klare Lebensdauer hat – nämlich, es kann nicht auf der obersten Ebene eines Skripts verwendet werden, da Variablen auf der obersten Ebene eines Skripts im Gültigkeitsbereich für alle zukünftigen Skripte auf der Seite sind, was praktisch bedeutet, dass die Ressource niemals freigegeben werden kann, wenn die Seite niemals entladen wird. Sie können es jedoch auf der obersten Ebene eines Moduls verwenden, weil der Modulbereich endet, wenn das Modul die Ausführung beendet.
Jetzt wissen wir, wann using
das Aufräumen durchführt. Aber wie wird es gemacht? using
erfordert, dass die Ressource das disposable-Protokoll implementiert. Ein Objekt ist disposable, wenn es die Methode [Symbol.dispose]()
hat. Diese Methode wird ohne Argumente aufgerufen, um die Bereinigung durchzuführen. Zum Beispiel kann im Falle des Lesers die [Symbol.dispose]
-Eigenschaft ein einfacher Alias oder Wrapper von releaseLock
sein:
// For demonstration
class MyReader {
// A wrapper
[Symbol.dispose]() {
this.releaseLock();
}
releaseLock() {
// Logic to release resources
}
}
// OR, an alias
MyReader.prototype[Symbol.dispose] = MyReader.prototype.releaseLock;
Durch das disposable-Protokoll kann using
alle Ressourcen konsistent freigeben, ohne zu wissen, welcher Ressourcentyp es ist.
Jeder Gültigkeitsbereich hat eine Liste von Ressourcen, die mit ihm verbunden sind, in der Reihenfolge, in der sie deklariert wurden. Wenn der Bereich verlassen wird, werden die Ressourcen in umgekehrter Reihenfolge freigegeben, indem ihre [Symbol.dispose]()
-Methode aufgerufen wird. Zum Beispiel wird im obigen Beispiel reader1
vor reader2
deklariert, also wird reader2
zuerst und dann reader1
freigegeben. Fehler, die beim Versuch, eine Ressource freizugeben, ausgelöst werden, verhindern nicht die Freigabe anderer Ressourcen. Dies ist konsistent mit dem try...finally
-Muster und berücksichtigt mögliche Abhängigkeiten zwischen den Ressourcen.
await using
ist using
sehr ähnlich. Die Syntax zeigt an, dass irgendwo ein await
passiert – nicht wenn die Ressource deklariert wird, sondern tatsächlich wenn sie freigegeben wird. await using
erfordert, dass die Ressource asynchron freigebbar ist, was bedeutet, dass sie eine [Symbol.asyncDispose]()
Methode hat. Diese Methode wird ohne Argumente aufgerufen und gibt ein Promise zurück, das sich löst, wenn die Bereinigung abgeschlossen ist. Dies ist nützlich, wenn die Bereinigung asynchron ist, wie fileHandle.close()
, in welchem Fall das Ergebnis der Freigabe nur asynchron bekannt sein kann.
{
await using fileHandle = open("file.txt", "w");
await fileHandle.write("Hello");
// fileHandle.close() is called and awaited
}
Weil await using
ein await
erfordert, ist es nur in Kontexten erlaubt, in denen await
ist, was async
-Funktionen und Top-Level-awaits
in Modulen einschließt.
Ressourcen werden sequentiell und nicht gleichzeitig bereinigt: Der Rückgabewert der [Symbol.asyncDispose]()
-Methode einer Ressource wird vor dem Aufruf der nächsten [Symbol.asyncDispose]()
-Methode erwartet.
Einige Punkte, die zu beachten sind:
using
undawait using
sind opt-in. Wenn Sie Ihre Ressource mitlet
,const
odervar
deklarieren, erfolgt keine automatische Freigabe, genau wie bei allen anderen nicht-disposablen Werten.using
undawait using
erfordern, dass die Ressource disposable (oder async disposable) ist. Wenn die Ressource nicht die Methode[Symbol.dispose]()
oder[Symbol.asyncDispose]()
hat, erhalten Sie einenTypeError
bei der Deklarationslinie. Die Ressource kann jedochnull
oderundefined
sein, sodass Sie Ressourcen bedingt erwerben können.- Wie
const
, könnenusing
- undawait using
-Variablen nicht neu zugewiesen werden, obwohl die Eigenschaften der Objekte, die sie enthalten, geändert werden können. Die[Symbol.dispose]()
/[Symbol.asyncDispose]()
Methode wird jedoch bereits zum Zeitpunkt der Deklaration gespeichert, sodass das Ändern der Methode nach der Deklaration die Bereinigung nicht beeinflusst. - Es gibt ein paar Fallstricke, wenn man Bereiche mit Ressourcen-Lebensdauer vermischt. Siehe
using
für einige Beispiele.
Die DisposableStack
- und AsyncDisposableStack
-Objekte
using
und await using
sind spezielle Syntaxen. Syntaxen sind praktisch und verbergen viel der Komplexität, aber manchmal müssen Sie Dinge manuell tun.
Ein häufiges Beispiel: Was, wenn Sie die Ressource nicht am Ende dieses Bereichs freigeben möchten, sondern in einem späteren Bereich? Betrachten Sie dies:
let reader;
if (someCondition) {
reader = stream.getReader();
} else {
reader = stream.getReader({ mode: "byob" });
}
Wie gesagt, using
ist wie const
: Es muss initialisiert sein und kann nicht neu zugewiesen werden, sodass Sie dies versuchen könnten:
if (someCondition) {
using reader = stream.getReader();
} else {
using reader = stream.getReader({ mode: "byob" });
}
Dies bedeutet jedoch, dass die gesamte Logik innerhalb des if
oder else
geschrieben werden muss, was zu viel Duplikation führt. Was wir tun möchten, ist, die Ressource in einem Bereich zu erwerben und sie in einem anderen freizugeben. Wir können dafür einen DisposableStack
verwenden, der ein Objekt ist, das eine Sammlung von disposablen Ressourcen hält und selbst disposable ist:
{
using disposer = new DisposableStack();
let reader;
if (someCondition) {
reader = disposer.use(stream.getReader());
} else {
reader = disposer.use(stream.getReader({ mode: "byob" }));
}
// Do something with reader
// Before scope exit, disposer is disposed, which disposes reader
}
Sie könnten eine Ressource haben, die das disposable-Protokoll noch nicht implementiert, sodass sie von using
abgelehnt wird. In diesem Fall können Sie adopt()
verwenden.
{
using disposer = new DisposableStack();
// Suppose reader does not have the [Symbol.dispose]() method,
// then it cannot be used with using.
// However, we can manually pass a disposer function to disposer.adopt
const reader = disposer.adopt(stream.getReader(), (reader) =>
reader.releaseLock(),
);
// Do something with reader
// Before scope exit, disposer is disposed, which disposes reader
}
Sie könnten eine Bereinigungsaktion ausführen müssen, die nicht mit einer bestimmten Ressource "verbunden" ist. Vielleicht möchten Sie einfach nur eine Nachricht wie "Alle Datenbankverbindungen geschlossen" protokollieren, wenn mehrere Verbindungen gleichzeitig geöffnet sind. In diesem Fall können Sie defer()
verwenden.
{
using disposer = new DisposableStack();
disposer.defer(() => console.log("All database connections closed"));
const connection1 = disposer.use(openConnection());
const connection2 = disposer.use(openConnection());
// Do something with connection1 and connection2
// Before scope exit, disposer is disposed, which first disposes connection1
// and connection2 and then logs the message
}
Sie möchten möglicherweise eine bedingte Freigabe durchführen – zum Beispiel nur erworbene Ressourcen freigeben, wenn ein Fehler aufgetreten ist. In diesem Fall können Sie move()
verwenden, um die Ressourcen zu bewahren, die ansonsten freigegeben würden.
class MyResource {
#resource1;
#resource2;
#disposables;
constructor() {
using disposer = new DisposableStack();
this.#resource1 = disposer.use(getResource1());
this.#resource2 = disposer.use(getResource2());
// If we made it here, then there were no errors during construction and
// we can safely move the disposables out of `disposer` and into `#disposables`.
this.#disposables = disposer.move();
// If construction failed, then `disposer` would be disposed before reaching
// the line above, disposing `#resource1` and `#resource2`.
}
[Symbol.dispose]() {
this.#disposables.dispose(); // Dispose `#resource2` and `#resource1`.
}
}
AsyncDisposableStack
ist wie DisposableStack
, aber zur Verwendung mit asynchronen disposable Ressourcen. Seine use()
-Methode erwartet ein async disposable, seine adopt()
-Methode erwartet eine asynchrone Bereinigungsfunktion und seine dispose()
-Methode erwartet einen asynchronen Callback. Es bietet eine [Symbol.asyncDispose]()
Methode. Sie können ihm weiterhin synchrone Ressourcen übergeben, wenn Sie einen Mix aus synchronen und asynchronen Ressourcen haben.
Das Referenzdokument für DisposableStack
enthält mehr Beispiele und Details.
Fehlerbehandlung
Ein Hauptanwendungsfall der Ressourcenmanagementfunktion ist sicherzustellen, dass Ressourcen immer freigegeben werden, selbst wenn ein Fehler auftritt. Lassen Sie uns einige komplexe Fehlerbehandlungsszenarien untersuchen.
Wir beginnen mit dem folgenden Code, der durch die Verwendung von using
gegen Fehler robust ist:
async function readUntil(stream, text) {
// Use `using` instead of `await using` because `releaseLock` is synchronous
using reader = stream.getReader();
let chunk = await reader.read();
while (!chunk.done && chunk.value !== text) {
console.log(chunk.toUpperCase());
chunk = await reader.read();
}
}
Angenommen, chunk
stellte sich als null
heraus. Dann wird toUpperCase()
einen TypeError
auslösen, was dazu führt, dass die Funktion beendet wird. Bevor die Funktion beendet wird, wird stream[Symbol.dispose]()
aufgerufen, was das Sperren des Streams aufhebt.
const stream = new ReadableStream({
start(controller) {
controller.enqueue("a");
controller.enqueue(null);
controller.enqueue("b");
controller.enqueue("c");
controller.close();
},
});
readUntil(stream, "b")
.catch((e) => console.error(e)) // TypeError: chunk.toUpperCase is not a function
.then(() => {
const anotherReader = stream.getReader();
// Successfully creates another reader
});
Daher unterdrückt using
keine Fehler: Alle auftretenden Fehler werden weiterhin geworfen, aber die Ressourcen werden direkt vorher geschlossen. Was passiert nun, wenn die Ressourcensäuberung selbst einen Fehler wirft? Lassen Sie uns ein noch konstruierteres Beispiel verwenden:
class MyReader {
[Symbol.dispose]() {
throw new Error("Failed to release lock");
}
}
function doSomething() {
using reader = new MyReader();
throw new Error("Failed to read");
}
try {
doSomething();
} catch (e) {
console.error(e); // SuppressedError: An error was suppressed during disposal
}
In dem Aufruf von doSomething()
werden zwei Fehler generiert: ein Fehler, der während doSomething
ausgelöst wird, und ein Fehler, der beim Freigeben von reader
aufgrund des ersten Fehlers ausgelöst wird. Beide Fehler werden zusammen geworfen, sodass das, was Sie gefangen haben, ein SuppressedError
ist. Dies ist ein spezieller Fehler, der zwei Fehler umschließt: Die error
-Eigenschaft enthält den späteren Fehler, und die suppressed
-Eigenschaft enthält den früheren Fehler, der durch den späteren Fehler "unterdrückt" wird.
Wenn wir mehr als eine Ressource haben und beide von ihnen während der Freigabe einen Fehler auslösen (dies sollte äußerst selten sein – es ist bereits selten, dass die Freigabe fehlschlägt!), dann wird jeder frühere Fehler durch den späteren Fehler unterdrückt und bildet eine Kette unterdrückter Fehler.
class MyReader {
[Symbol.dispose]() {
throw new Error("Failed to release lock on reader");
}
}
class MyWriter {
[Symbol.dispose]() {
throw new Error("Failed to release lock on writer");
}
}
function doSomething() {
using reader = new MyReader();
using writer = new MyWriter();
throw new Error("Failed to read");
}
try {
doSomething();
} catch (e) {
console.error(e); // SuppressedError: An error was suppressed during disposal
console.error(e.suppressed); // SuppressedError: An error was suppressed during disposal
console.error(e.error); // Error: Failed to release lock on reader
console.error(e.suppressed.suppressed); // Error: Failed to read
console.error(e.suppressed.error); // Error: Failed to release lock on writer
}
- Der
reader
wird zuletzt freigegeben, sodass sein Fehler der letzte ist und daher alles andere unterdrückt: Er erscheint alse.error
. - Der
writer
wird zuerst freigegeben, sodass sein Fehler später als der ursprüngliche austretende Fehler ist, aber früher als derreader
-Fehler: Er erscheint alse.suppressed.error
. - Der ursprüngliche Fehler über das "Fehlgeschlagene Lesen" ist der früheste Fehler, sodass er als
e.suppressed.suppressed
erscheint.
Beispiele
Automatisches Freigeben von Objekt-URLs
Im folgenden Beispiel erstellen wir eine Objekt-URL zu einem Blob (in einer realen Anwendung würde dieser Blob von irgendwoher abgerufen, wie einer Datei oder einer Abruffunktion), damit wir den Blob als Datei herunterladen können. Um ein Ressourcenleck zu vermeiden, müssen wir die Objekt-URL mithilfe von URL.revokeObjectURL()
freigeben, sobald sie nicht mehr benötigt wird (das heißt, wenn der Download erfolgreich gestartet wurde). Da die URL selbst nur eine Zeichenfolge ist und daher nicht das disposable-Protokoll implementiert, können wir url
nicht direkt mit using
deklarieren; daher erstellen wir einen DisposableStack
, der als Freigeber für url
dient. Die Objekt-URL wird sofort gelöscht, wenn disposer
aus dem Gültigkeitsbereich geht, was entweder dann der Fall ist, wenn link.click()
abgeschlossen ist oder ein Fehler auftritt.
const downloadButton = document.getElementById("download-button");
const exampleBlob = new Blob(["example data"]);
downloadButton.addEventListener("click", () => {
using disposer = new DisposableStack();
const link = document.createElement("a");
const url = disposer.adopt(
URL.createObjectURL(exampleBlob),
URL.revokeObjectURL,
);
link.href = url;
link.download = "example.txt";
link.click();
});
Automatisches Abbrechen von laufenden Anfragen
Im folgenden Beispiel rufen wir eine Liste von Ressourcen gleichzeitig mit Promise.all()
ab. Promise.all()
schlägt fehl und weist das resultierende Promise zurück, sobald eine Anfrage fehlgeschlagen ist; jedoch laufen die anderen ausstehenden Anfragen weiter, obwohl ihre Ergebnisse für das Programm unzugänglich sind. Um zu verhindern, dass diese verbleibenden Anfragen unnötig Ressourcen verbrauchen, müssen wir laufende Anfragen automatisch stornieren, sobald Promise.all()
abgeschlossen ist. Wir implementieren die Stornierung mit einem AbortController
und übergeben dessen signal
an jeden fetch()
-Aufruf. Wenn Promise.all()
erfolgreich ist, gibt die Funktion normal zurück und der Controller bricht ab, was harmlos ist, da keine ausstehende Anfrage zu stornieren ist; wenn Promise.all()
abgelehnt wird und die Funktion einen Fehler wirft, bricht der Controller ab und storniert alle ausstehenden Anfragen.
async function getAllData(urls) {
using disposer = new DisposableStack();
const { signal } = disposer.adopt(new AbortController(), (controller) =>
controller.abort(),
);
// Fetch all URLs in parallel
// Automatically cancel any incomplete requests if any request fails
const pages = await Promise.all(
urls.map((url) =>
fetch(url, { signal }).then((response) => {
if (!response.ok)
throw new Error(
`Response error: ${response.status} - ${response.statusText}`,
);
return response.text();
}),
),
);
return pages;
}
Fallstricke
Die Ressourcensäuberungssyntax bietet viele starke Fehlerbehandlungsgarantien, die sicherstellen, dass die Ressourcen aufgeräumt werden, egal was passiert, aber es gibt einige Fallstricke, die Sie dennoch erleben könnten:
- Vergessen,
using
oderawait using
zu verwenden. Die Ressourcenmanagementsyntax ist nur da, um Ihnen zu helfen, wenn Sie wissen, dass Sie sie benötigen, aber es gibt nichts, das Sie darauf aufmerksam macht, wenn Sie sie vergessen! Leider gibt es keinen guten Weg, dies im Voraus zu verhindern, da es keine syntaktischen Hinweise gibt, dass etwas eine disposable Ressource ist, und selbst für disposable Ressourcen möchten Sie sie möglicherweise ohne automatische Freigabe deklarieren. Sie benötigen wahrscheinlich einen Typenprüfer in Kombination mit einem Linter, um diese Probleme zu erkennen, wie typescript-eslint (das noch plant, an dieser Funktion zu arbeiten). - Verwenden nach dem Freigeben. Im Allgemeinen stellt die
using
-Syntax sicher, dass eine Ressource freigegeben wird, wenn sie aus dem Gültigkeitsbereich hinausgeht, aber es gibt viele Möglichkeiten, einen Wert über seine Bindungsvariable hinaus beizubehalten. JavaScript hat keinen Besitzmechanismus wie Rust, sodass Sie einen Alias deklarieren können, derusing
nicht verwendet, oder die Ressource in einer closure beibehalten können, etc. Dieusing
-Referenz enthält viele Beispiele für solche Fallstricke. Wieder gibt es keinen guten Weg, dies in einem komplizierten Kontrollfluss richtig zu erkennen, sodass Sie vorsichtig sein müssen.
Die Ressourcenmanagementfunktion ist kein Allheilmittel. Sie stellt definitiv eine Verbesserung gegenüber dem manuellen Aufrufen der Freigabemethoden dar, aber sie ist nicht smart genug, um alle Ressourcenmanagementfehler zu verhindern. Sie müssen dennoch vorsichtig sein und die Semantik der Ressourcen, die Sie verwenden, verstehen.
Fazit
Hier sind die wichtigsten Komponenten des Ressourcenmanagementsystems:
using
undawait using
-Deklarationen für die automatische Ressourcenfreigabe.- Die disposable- und async disposable-Protokolle, die durch den Einsatz von
Symbol.dispose
undSymbol.asyncDispose
von Ressourcen implementiert werden. - Die
DisposableStack
- undAsyncDisposableStack
-Objekte für Fälle, in denenusing
undawait using
nicht geeignet sind.
Mit der ordnungsgemäßen Nutzung dieser APIs können Sie Systeme erstellen, die mit externen Ressourcen interagieren und stark und robust gegen alle Fehlerbedingungen bleiben, ohne viel Boilerplate-Code.