リソースの解放を自動化できるusing宣言
本稿最後に紹介するのは、using宣言です。
リソースの解放を伴うコードパターン
本節で紹介するusingキーワードは、リソースの解放と関連するものです。そこで、まず、リソースの解放に関するコードパターンを紹介します。
例えば、データベース接続やファイルの読み書き、ネットワークへの接続など、いわゆるリソースと呼ばれるものを利用するコードの場合、それらのリソースオブジェクト、例えば、データベース接続オブジェクトやファイルオブジェクトなどは、処理の最後に解放処理を必ず行う必要があります。これらの解放処理は、リソースオブジェクトにもともと用意されている解放メソッドを実行することが、通常です。そして、この解放処理をどれだけ確実に行えるかが、すなわち、解放メソッドをどれだけ確実に実行できるかが、コーディングの腕の見せ所です。
例えば、リスト10のような処理コードを考えます。
const connection = new Connection(); // (1) : // (2) connection.close(); // (3)
リスト10の(1)は、何かのリソース(例えばデータベース)を利用するためのオブジェクトを生成するコードとします。Connectionクラスをnewすることによって生成したconnectionオブジェクトは、まさにリソースオブジェクトです。以降、リスト10の(2)では、このconnectionを使って、さまざまなデータ処理を行います。そして、このconnectionオブジェクトの解放メソッドがclose()とするならば、(3)のようなコードを最終的に実行する必要があります。
しかし、もしも(2)でエラーが発生すると、(3)のコードは実行されないことになり、リソースが解放されません。そこで、通常は、リスト11のように、(2)をtryブロックで囲み、(3)をfinallyブロックに記述します。
const connection = new Connection(); // (1) try { : // (2) } finally { connection.close(); // (3) }
using宣言変数はリソース解放が自動化される
このようなコードパターンに対応するために導入されたのが、usingキーワードです。このusingは、最新のTypeScriptであるバージョン5.2で導入されたものであり、ECMAScriptでは、現在、ステージ3の段階です。このキーワードを利用すると、リスト11は、リスト12のように記述できます。
using connection = new Connection(); // (1) : // (2)
ポイントは、(1)の変数宣言です。constでもletでもなく、usingで宣言しています。このように、解放処理が必要なリソースオブジェクト変数に対して、usingで宣言するだけで、その後の処理は、(2)のように、tryブロックで囲む必要もなく、close()のような解放処理メソッドの実行も不要です。
リソースオブジェクトへの細工
ただし、このusingキーワードを利用する場合は、リソースオブジェクトそのものに細工を施す必要があります。それは、Disposableインターフェースを実装したオブジェクトにしておくというものです。このDisposableインターフェースは、リスト13の内容です。
interface Disposable { [Symbol.dispose](): void; }
Symbol.disposeはSymbolクラスに新たに導入された定数であり、このことから、Disposableの実装オブジェクトでは、それをメソッド名とするメソッドを実装しなければならないということになります。そして、このメソッド内に、リソースの解放処理を記述することになっています。そのため、例えば、リスト12の(1)でのConnectionクラスのコードは、リスト14のようなものになります。
class Connection implements Disposable { : constructor() { // リソース利用に必要な処理 } : close(): void { // リソースの解放処理 } [Symbol.dispose](): void { // (1) this.close(); // (2) } }
リスト14で注目すべきは、(1)と(2)です。インターフェースに定義された通りに(1)のメソッドを実装し、(2)のように、本来、finallyなどで実行していたclose()メソッドを実行します。このコードにより、このクラスをnewしたオブジェクトをusing宣言しておけば、その変数の利用が不要となった際に、自動的に[Symbol.dispose]()メソッドが実行され、close()メソッドが実行されます。結果、自動的にリソースの解放処理が実行されます。
非同期でのリソース解放オブジェクト
これらのusingキーワードは、非同期での解放処理にも対応しています。例えば、リスト14のclose()メソッドが非同期メソッドとした場合、Connectionクラスはリスト15のようになります。
class Connection implements AsyncDisposable { // (1) : async close(): void { // (2) // リソースの解放処理 } async [Symbol.asyncDispose](): Promise<void> { // (3) await this.close(); // (4) } }
リスト15では、リソースの解放メソッドclose()が、(2)のようにasyncキーワードが付与されて非同期処理メソッドとなっています。となると、このclose()を実行する場合は、(4)のようにawaitを付与します。その場合は、この解放処理を自動実行するメソッドにも、(3)のようにasyncを付与する必要があります。さらに、メソッド名を、[Symbol.dispose]ではなく、[Symbol.asyncDispose]とします。そして、このメソッドが定義されたインターフェースが、(1)でimplementsしているAsyncDisposableであり、リスト16のコードとなります。
interface AsyncDisposable { [Symbol.asyncDispose](): Promise<void>; }
非同期でのリソース解放オブジェクトでのusing
このような非同期で解放を行うリソースオブジェクトを取得する場合のusingの利用方法は、リスト17のようになります。usingの前にawaitをつけるだけです。
await using connection = new Connection();
リソース解放のみ自動実行できるクラス
ここまで紹介した方法は、リソースオブジェクトそのものをDisposableインターフェース、あるいは、AsyncDisposableインターフェースを実装したオブジェクトとして、定義できる場合の話です。リソースオブジェクトによっては、そのようなものばかりではありません。その際に便利なのが、DisposableStackクラス、および、その非同期処理用であるAsyncDisposableStackクラスです。このクラスを利用するコードは、リスト18のようになります。
const connection = new Connection(); // (1) using cleanup = new DisposableStack(); // (2) cleanup.defer( // (3) () => { connection.close(); // (4) } ); :
まず、リスト18の(1)のようにリソースオブジェクトを取得します。このリソースオブジェクトには、[Symbol.dispose]()メソッドや[Symbol.asyncDispose]()メソッドが含まれていないとします。すると、usingによる宣言はできません。代わりに、(2)のようにDisposableStack、あるいは、AsyncDisposableStackクラスをnewした変数をusing宣言とします。
そして、そのDisposableStack/AsyncDisposableStackオブジェクト(リスト18ではcleanup)に対して、(3)のようにdefer()メソッドを実行します。このdefer()メソッドの引数であるアロー関数内で、(4)のようにリソースオブジェクトの解放メソッドを実行します。
このコードパターンにより、Disposable/AsyncDisposableインターフェースを実装できないリソースオブジェクトでも、using宣言による自動解放が可能となります。
実行環境がまだ未整備
ただし、このusingに関しては、リリースして間もないこともあり、まともに動作しない環境も多々ある点には注意してください。例えば、本稿執筆時点では最新環境であるNode.jsのバージョン20.0.9、TypeScriptのバージョン5.2.2でも、リスト18のサンプルコードをコンパイルしようとすると、DisposableStackクラスが存在しないというエラーになります。
また、コンパイルオプションとして、リスト19のような設定を行う必要があります。この点も注意してください。
{ "compilerOptions": { "target": "es2022", "lib": ["es2022", "esnext.disposable", "dom"] } }
まとめ
TypeScriptのバージョン5.2までに導入された新機能をテーマごとに紹介する本連載の第1回目はいかがでしたでしょうか。
今回は、初回ということもあり、概説と、テーマとしては1回分には満たない分量の詰め合わせとして、新しい演算子、トップレベルawait、using宣言を紹介しました。
次回は、タプルに関するアップデートをテーマに紹介します。