📋 目錄
這是 TC39 新語法深度解析系列的第二篇。忘記關閉檔案、遺漏 DB 連線、視窗外監聽忘記移除——這些都是每個工程師曾經踩過的坑。Explicit Resource Management 是 TC39 對這個問題的標準化答案。
問題:忘記 cleanup 是代價昂貴的錯誤
// 過去的做法:try-finally
async function processFile(path: string) {
const file = await fs.open(path, 'r');
try {
const content = await file.read();
return content;
} finally {
await file.close(); // 終於執行
}
}
// 問題:如果在 try 裡有 early return 或拋出錯誤
// finally 仍然會執行——這是對的
// 但如果錯誤發生在 await file.read() 和 await file.close() 中間
// 某些資源釋放順序可能不正確
更糟的是,當你需要管理多個資源時,try-finally 會快速失控:
async function multiResource() {
const conn = await db.connect();
const file = await fs.open('data.json', 'r');
const socket = await net.connect('api.example.com', 443);
try {
// 實際工作...
} finally {
// 必須按正確順序釋放:後進先出
if (socket) await socket.close();
if (file) await file.close();
if (conn) await conn.close();
}
}
using 的語法:資源自動釋放
基本用法
// using 關鍵字:離開作用域時,自動呼叫 .dispose()
function readConfig() {
using file = { path: 'config.json' };
// 模擬:打開文件
console.log('Reading config...');
// 離開函式時,file[Symbol.dispose]() 自動呼叫
}
const content = readConfig();
// console output:
// Reading config...
// Closing config.json (自動)
實際與 async 資源
// await using:用於 async 資源
async function fetchUserData(userId: string) {
await using conn = await db.connect(); // 離開時自動 await conn[Symbol.asyncDispose]()
const user = await conn.query(
'SELECT * FROM users WHERE id = ?',
[userId]
);
return user;
// conn.dispose() 在函式結束時自動呼叫
}
Symbol.dispose 與 Symbol.asyncDispose
任何物件只要實作了 [Symbol.dispose]() 方法,就可以用 using 管理:
// 資源類別:實作 Symbol.dispose
class DatabaseConnection {
async [Symbol.asyncDispose]() {
console.log('Closing database connection...');
await this.client.end();
}
}
async function query() {
await using db = new DatabaseConnection(config);
const result = await db.query('SELECT 1');
return result;
// [Symbol.asyncDispose] 在這裡自動呼叫
}
DisposableStack:管理多個資源
當你需要一次管理多個資源,DisposableStack 是最優雅的解決方案:
function batchProcess() {
// 建立一個堆疊來管理多個資源
using stack = new DisposableStack();
const file = stack.use(openFile('data.csv'));
const db = stack.use(await dbPool.connect());
const cache = stack.use(new LRUCache());
// 如果中途出錯,stack.use() 確保所有資源在正確順序下被釋放
const result = process(file, db, cache);
return result;
// 所有資源自動釋放(後進先出)
}
DisposableStack 的 use() 方法讓你可以在一個 stack 裡追蹤多個資源,並確保它們**以正確順序(後進先出)**被釋放。
實用範例:從檔案 I/O 到 Canvas
檔案處理
// before:try-finally 冗長
async function readAndProcessFile(path: string) {
let file: FileHandle;
try {
file = await fs.open(path, 'r');
const content = await file.read();
return process(content);
} finally {
if (file) await file.close();
}
}
// after:using 簡潔
async function readAndProcessFile(path: string) {
await using file = await fs.open(path, 'r');
return process(await file.read());
// file.close() 自動
}
Canvas Context
// Canvas 2D context 的 restore 總是忘記加?
function drawChart(canvas: HTMLCanvasElement) {
const ctx = canvas.getContext('2d');
using _ = {
[Symbol.dispose]() {
ctx.restore(); // 總是執行,無論正常還是異常退出
}
};
ctx.save();
ctx.translate(100, 100);
ctx.rotate(Math.PI / 4);
drawSomething(ctx);
// _[Symbol.dispose]() 在這裡自動執行,ctx.restore() 必定被呼叫
}
TypeScript 5.2+ 支援
using 和 await using 現已進入 TypeScript 5.2+:
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"]
}
}
// 確認 TypeScript 版本
// npx tsc --version // 需要 5.2+
function example() {
using resource = createResource();
// 使用 resource...
}
// [Symbol.dispose]() 在函式結束時自動呼叫
與 try-finally 的比較
| 維度 | try-finally | using |
|---|---|---|
| 語法簡潔度 | 需要明確寫 finally | 自動,無需 extra block |
| 多資源管理 | 需要巢狀 try 或手動管理 | DisposableStack |
| 資源釋放順序 | 手動控制,容易出錯 | 自動後進先出 |
| 編譯器優化 | 無特殊處理 | 可被編譯器優化 |
| 可組合性 | 困難 | DisposableStack 可以组合 |
總結
using 是那種「一旦用過就不想回去」的語法。它的價值:
- 編譯器強制執行:不再可能忘記 cleanup
- 程式碼更短:不需要
try-finally包裝 - 資源釋放順序正確:後進先出由 runtime 保證
- 可組合:DisposableStack 讓多資源管理優雅
這是 TC39 新語法深度解析系列的第二篇。下一篇:Set 新方法 — union、intersection、difference 終於原生支援。
延伸閱讀
- TypeScript 6.0 RC 升級指南 — 支援 Explicit Resource Management
本文基於 TC39 Explicit Resource Management 提案(Stage 4)整理,TypeScript 5.2+ 已支援。