メインコンテンツまでスキップ

新しい変数宣言 using

この章の最初のページでletconstという変数宣言について学びました。

using宣言(using declaration)とは、JavaScriptに導入される新しい変数宣言であり、執筆時点のECMAScriptプロポーザルではStage3の機能です。TypeScriptでは5.2からサポートされています。

using宣言された変数がスコープを抜けるときに、その変数に紐づくリソースについて自動的にクリーンアップ処理が実行されることで「明示的なリソース管理 (Explicit Resource Management)」を実現できます。

Denoを使う理由

このページでは次の理由から Deno ランタイムのファイル API をサンプルコードに多用しています。

  1. Symbol.dispose が標準で実装済み: Deno の FsFileusing 宣言がそのまま使えるオブジェクトの実例です。
  2. リソースの概念が直感的: ファイルハンドルやネットワーク接続は、ブラウザの Web API よりもコンピュータのリソース管理に近く、「開いたら閉じる」というライフサイクルが分かりやすいです。

リソースとは

using宣言を理解するための前提として「リソース (resource)」の概念を知っておく必要があります。リソース、あるいはシステムリソースとは、簡単に言えば、プログラムがOSから借りて使うものであり、使い終わったら必ず返す必要のあるコンポーネントのことで、たとえば次のようなものを指します。

  • ファイル(ファイルハンドル)
  • ネットワーク接続(ネットワークソケット)
  • DB接続(データベースコネクション)
  • メモリ領域(ヒープメモリ)
注意

このページでの「メモリ」とは「ヒープメモリ」のことを指していることに注意してください。「スタックメモリ」はGCとは関係なく解放されます。これらのメモリの違いについての詳細は次のページなどを参照してください。

https://doc.rust-jp.rs/book-ja/ch04-01-what-is-ownership.html

JavaScript/TypeScriptの文脈において、特にメモリ領域は実行環境が持つGC(ガベージコレクタ)により自動的に管理されます。そのためC言語やZig言語のようにプログラマーが明示的なメモリ解放を行なう必要がありません。

C言語の明示的なメモリ解放
c
#include <stdlib.h>
int main(void) {
// メモリを確保
int *array = malloc(5 * sizeof(int));
array[0] = 42;
// 明示的にメモリを解放しなければならない
free(array);
return 0;
}
C言語の明示的なメモリ解放
c
#include <stdlib.h>
int main(void) {
// メモリを確保
int *array = malloc(5 * sizeof(int));
array[0] = 42;
// 明示的にメモリを解放しなければならない
free(array);
return 0;
}

GCはメモリ領域以外のリソースの管理は行わないため、ファイルやネットワークソケットといった非メモリリソースの管理はプログラマーが明示的に行なう必要があります。たとえば、次の Deno 環境で作成されたファイル読み込みの処理では、ファイルハンドルは次のように読み取りのために open() したら、利用終了時には close() するという処理を行なう必要があります。

resource.ts
ts
async function readFile(fileName: string): Promise<void> {
// ファイルを開く
const file = await Deno.open(fileName);
try {
// ファイルを読み取る
const buffer = new Uint8Array(5);
const bytesRead = await file.read(buffer);
console.log(bytesRead);
} finally {
// 必ずファイルを閉じる(リソースを解放)
file.close();
}
}
resource.ts
ts
async function readFile(fileName: string): Promise<void> {
// ファイルを開く
const file = await Deno.open(fileName);
try {
// ファイルを読み取る
const buffer = new Uint8Array(5);
const bytesRead = await file.read(buffer);
console.log(bytesRead);
} finally {
// 必ずファイルを閉じる(リソースを解放)
file.close();
}
}
備考

Deno.open は Deno ランタイムを利用した環境で利用できるファイルを開くためのAPIです。次のような型を持ちます。

ts
open(
path: string | URL,
options?: OpenOptions,
): Promise<FsFile>
ts
open(
path: string | URL,
options?: OpenOptions,
): Promise<FsFile>

メモリやファイルといったリソースは利用後に必ず解放する必要があります。この解放処理を忘れると、リソースリークという問題が発生します。

リソースリークとは

リソースリーク (Resource leak)」とは、リソースの使用後に解放処理を忘れることで、そのリソースが無駄に占有され続ける問題のことです。たとえば、メモリ解放を忘れた場合には「メモリリーク (Memory leak)」と呼ばれる問題となり、プログラムの使用したメモリ領域が解放されず残されることでメモリ容量が徐々に減っていってしまいます。

次のようにリソースの種類により、リークの問題は呼称が異なりますが、一般にはリソースリークと呼ばれます。

  • ファイルハンドルリーク
  • ソケットリーク
  • DB接続リーク

メモリリーク以外のリソースリークでも、システムパフォーマンスの低下やクラッシュといった問題が発生する可能性があります。

リソースの使用後には必ず解放を行いたいところですが、手動で書かなければいけない場合には解放し忘れてしまう場合もあるでしょう。GCがメモリ解放を自動的に行ってくれるように、非メモリリソースの解放も自動的に行ってくれたら楽になることが想像できますね。

usingの登場

そこでusing宣言が登場しました。using宣言で宣言された変数に紐づけられたリソースは、その変数がスコープを抜けるときに [Symbol.dispose]() メソッドが呼び出され、リソースの解放処理が自動的に実行されます。

ts
{
// 変数初期化によるリソースの確保
using file = Deno.openSync(fileName);
file.writeSync(data);
file.readSync(buffer);
} // スコープを抜けると自動的に `file` に紐づくリソースの解放処理が呼ばれる
ts
{
// 変数初期化によるリソースの確保
using file = Deno.openSync(fileName);
file.writeSync(data);
file.readSync(buffer);
} // スコープを抜けると自動的に `file` に紐づくリソースの解放処理が呼ばれる

スコープ脱出のタイミングでリソース解放が行われる、つまり、コードの構造により自動的にリソースの解放タイミングが決まります。この using 宣言を使うことで先ほどの resource.ts は次のように書き換えることができます。

resource.ts
ts
async function readFile(fileName: string): Promise<void> {
// ファイルを開く
using file = await Deno.open(fileName);
// ファイルを読み取る
const buffer = new Uint8Array(5);
const bytesRead = await file.read(buffer);
console.log(bytesRead);
} // スコープ脱出時に自動的にリソース解放
resource.ts
ts
async function readFile(fileName: string): Promise<void> {
// ファイルを開く
using file = await Deno.open(fileName);
// ファイルを読み取る
const buffer = new Uint8Array(5);
const bytesRead = await file.read(buffer);
console.log(bytesRead);
} // スコープ脱出時に自動的にリソース解放

冒頭で使った「明示的なリソース管理」とは、このようにusing宣言で定義された変数に紐づくリソースの解放タイミング、ひいてはライフタイム(生存期間)そのものを、スコープというコードの構造によって明確に表すことができるということです。つまり、using が付いていることでリソース解放タイミングが誰が見ても一目でわかるようになっています。

DisposableとSymbol.dispose

using 宣言で使えるオブジェクトは、Disposable インターフェースを実装している、つまり [Symbol.dispose]() メソッドを持つ必要があります。

ts
const getConnection = (host: string): Disposable => {
console.log(`接続を開く: ${host}`);
return {
[Symbol.dispose]() {
console.log(`接続を閉じる: ${host}`);
},
};
};
 
{
using connection = getConnection("localhost");
// ...
} // ここで自動的に「接続を閉じる: localhost」が出力される
ts
const getConnection = (host: string): Disposable => {
console.log(`接続を開く: ${host}`);
return {
[Symbol.dispose]() {
console.log(`接続を閉じる: ${host}`);
},
};
};
 
{
using connection = getConnection("localhost");
// ...
} // ここで自動的に「接続を閉じる: localhost」が出力される
RAIIパターン

実は、このようなパターンは後ほど詳しく解説しますが、RAII(Resource Acquisition is Initialization)パターンと呼ばれ、他のプログラミング言語にも同様のパターンを見ることができます。

なお、using を使って宣言した変数は const 宣言による変数と同様にブロックスコープの変数として宣言され、再代入を行なうことができません。
また、using 宣言した変数の初期化として使える値は nullundefined または上述した Disposable インターフェースを実装したオブジェクトのみとなります。

ts
using t1 = null;
using t2 = undefined;
using t3 = {
[Symbol.dispose]() {},
};
 
using t4 = 1;
The initializer of a 'using' declaration must be either an object with a '[Symbol.dispose]()' method, or be 'null' or 'undefined'.2850The initializer of a 'using' declaration must be either an object with a '[Symbol.dispose]()' method, or be 'null' or 'undefined'.
ts
using t1 = null;
using t2 = undefined;
using t3 = {
[Symbol.dispose]() {},
};
 
using t4 = 1;
The initializer of a 'using' declaration must be either an object with a '[Symbol.dispose]()' method, or be 'null' or 'undefined'.2850The initializer of a 'using' declaration must be either an object with a '[Symbol.dispose]()' method, or be 'null' or 'undefined'.

await using

クリーンアップ処理自体が非同期の場合には、await using 宣言を使います。await using はスコープ脱出時に [Symbol.asyncDispose]()await して呼び出します。

await usingAsyncDisposable インターフェースを実装したオブジェクトが対象となります。

ts
const getConnection = (host: string): AsyncDisposable => {
console.log(`接続を開く: ${host}`);
return {
async [Symbol.asyncDispose]() {
// 非同期のクリーンアップ処理(例: ネットワーク越しの切断)
await Promise.resolve();
console.log(`接続を閉じる: ${host}`);
},
};
};
 
{
await using connection = getConnection("localhost");
// ...
} // ここで非同期の「接続を閉じる: localhost」が await される
ts
const getConnection = (host: string): AsyncDisposable => {
console.log(`接続を開く: ${host}`);
return {
async [Symbol.asyncDispose]() {
// 非同期のクリーンアップ処理(例: ネットワーク越しの切断)
await Promise.resolve();
console.log(`接続を閉じる: ${host}`);
},
};
};
 
{
await using connection = getConnection("localhost");
// ...
} // ここで非同期の「接続を閉じる: localhost」が await される

ただし、await using はスコープ脱出時にまず [Symbol.asyncDispose]() を探し、なければ [Symbol.dispose]() にフォールバックします。そのため AsyncDisposable だけでなく Disposable を実装したオブジェクトにも使えます。

usingawait using の使い分けは次のとおりです。

宣言対応インターフェースクリーンアップ
usingDisposable (Symbol.dispose)同期
await usingAsyncDisposable (Symbol.asyncDispose) を優先、なければ Disposable (Symbol.dispose) にフォールバック非同期(フォールバック時は同期)

なお、await usingasync 関数またはトップレベル await が使える環境でのみ利用できます。

他の言語でのパターン

RAIIパターン

RAII(Resource Acquisition Is Initialization)パターンとは、文字通り「リソース取得は初期化」を意味しており、リソースの確保と解放を変数の初期化と破棄に結びつけるというプログラミングパターンを表します。

JavaScriptの using を含め、次に挙げるようなプログラミング言語では類似のRAIIパターンを採用しています。

C#のusing句

JavaScriptの using に似ているのがC#の using です。using 句に指定されたオブジェクトは IDisposable インターフェースを実装している必要があります。

IDisposable インターフェースは Dispose メソッドを持ちます。

C#のIDisposableインターフェース
csharp
public interface IDisposable
{
void Dispose();
}
C#のIDisposableインターフェース
csharp
public interface IDisposable
{
void Dispose();
}

実装例として、DB接続クラスに IDisposable を実装すると次のようになります。

C#のIDisposableインターフェースの実装
csharp
class Connection : IDisposable
{
private readonly string host;
public Connection(string host)
{
this.host = host;
Console.WriteLine($"接続を開く: {host}");
}
public void Dispose()
{
Console.WriteLine($"接続を閉じる: {host}");
}
}
C#のIDisposableインターフェースの実装
csharp
class Connection : IDisposable
{
private readonly string host;
public Connection(string host)
{
this.host = host;
Console.WriteLine($"接続を開く: {host}");
}
public void Dispose()
{
Console.WriteLine($"接続を閉じる: {host}");
}
}

この Connection クラスを using 句で使うと、スコープ脱出時に自動的に Dispose が呼ばれます。

C#のusing句
csharp
using(var connection = new Connection("localhost"))
{
// ...
} // スコープを抜けるときに自動的にDisposeが呼ばれる
C#のusing句
csharp
using(var connection = new Connection("localhost"))
{
// ...
} // スコープを抜けるときに自動的にDisposeが呼ばれる

IDisposable インターフェースを実装していることで、スコープ脱出時に、IDisposableインターフェースの Dispose メソッドによるリソース解放が行われます。

C# 8.0以降では、JavaScriptの using により似ている次のような宣言形式も使えます。

C#のusing var
csharp
{
using var connection = new Connection("localhost");
// ...
} // スコープを抜けるときに自動的にDisposeが呼ばれる
C#のusing var
csharp
{
using var connection = new Connection("localhost");
// ...
} // スコープを抜けるときに自動的にDisposeが呼ばれる

Rustのdropメソッド

RustもRAIIパターンを採用しており、所有権という概念のもとで、メモリを含むあらゆるリソースの解放タイミングをスコープの脱出時に定めており、コードの構造によって解放のタイミングが決定されます。これによって、リソースリークを静的に防ぎ安全性を担保します。

いわゆる「所有権」とは、リソースの管理責任であり、所有者となる変数はこの所有権を持ち、所有権を移動したり、値の複製で新たな所有権を生成することが可能で、所有権を持つ変数がスコープを脱出したときに、リソースの解放処理が呼び出されることになります。このような所有権に基づいたリソース管理はまさに、RAIIに基づいたリソース管理の方法となっています。

非メモリリソースについてのスコープ脱出時の自動解放を実現しているのが、Rustの Drop トレイトに存在する drop というメソッドです。変数がスコープを抜けるときにはこのメソッドが自動的に呼び出されて、実装されているリソース解放の処理を行います。

Rustのdropメソッド
rust
struct Connection {
host: String,
}
impl Connection {
fn new(host: &str) -> Self {
println!("接続を開く: {}", host);
Connection { host: host.to_string() }
}
}
impl Drop for Connection {
fn drop(&mut self) {
// スコープを抜けるときに自動的に呼ばれる
println!("接続を閉じる: {}", self.host);
}
}
fn main() {
{
let connection = Connection::new("localhost");
// ...
} // ここで自動的に「接続を閉じる: localhost」が出力される
}
Rustのdropメソッド
rust
struct Connection {
host: String,
}
impl Connection {
fn new(host: &str) -> Self {
println!("接続を開く: {}", host);
Connection { host: host.to_string() }
}
}
impl Drop for Connection {
fn drop(&mut self) {
// スコープを抜けるときに自動的に呼ばれる
println!("接続を閉じる: {}", self.host);
}
}
fn main() {
{
let connection = Connection::new("localhost");
// ...
} // ここで自動的に「接続を閉じる: localhost」が出力される
}

Rustでは using のような特別な宣言は不要で、すべての変数がデフォルトでRAIIの対象となります。

なお、メモリリソースの解放はこの drop() の実装とは関係なく、コンパイラがコンパイル時にスコープ脱出時のタイミングに解放処理を差し込みます。これによって、基本的にすべてのリソースの解放がスコープ脱出時として定められます。

各言語のRAIIパターン比較

TypeScript・C#・Rustそれぞれの仕組みを比較すると次のとおりです。

比較項目TypeScriptC#Rust
インターフェース/トレイトDisposableIDisposableDrop
クリーンアップメソッド[Symbol.dispose]()Dispose()drop(&mut self)
宣言構文using / await usingusing 句 / using var不要(暗黙)
強制力オプトイン(明示的に using が必要)オプトイン(明示的に using が必要)すべての変数が対象

3者間の大きな違いは強制力にあります。TypeScriptとC#では using を書かなければRAIIは機能せず、うっかり書き忘れるとリソースリークが起きます。一方Rustでは所有権システムにより、すべての変数がスコープ脱出時に自動的に drop される仕組みになっており、書き忘れが原理的に発生しません。