代表的な面接トピック

コーディング面接:JavaScriptの明示的リソース管理を安全に使用するにはどうすればよいか?

コーディング難しい
Offer.cc 編集チーム公開日 更新日

質問

リクエストハンドラーがファイル、タイマー、および非同期ロックを開きます。Symbol.dispose、Symbol.asyncDispose、using、await using を使用して、成功、失敗、キャンセルの各ケースですべてのリソースが解放されるようにする方法を示してください。スコープ、破棄順序、エラーハンドリング、機能検出、およびテストについて説明してください。

プロンプトとスコープ

ハンドラーは、ブロック境界でライフタイムが終了しなければならない同期的および非同期的なリソースを取得します。JavaScriptの明示的リソース管理プロトコルを使用して安全なラッパーを設計し、構築、本体の実行、またはクリーンアップが失敗したときに何が起こるかを説明してください。回答では、言語構文を実行時およびライブラリのサポートと区別する必要があります。TypeScript 5.2は構文の型チェックが可能ですが、本番環境での利用可否は依然としてターゲットエンジンとトランスパイル戦略に依存します。

これは、主要なスキルがフレームワークの選択ではなく、リソースのライフタイムの推論とフォールトセーフな実装であるため、coding の問題です。

面接官が評価するポイント

第一に、同期的なクリーンアップのための [Symbol.dispose]() と、awaitする必要があるクリーンアップのための [Symbol.asyncDispose]() を備えたリソースを定義できるかどうか。

第二に、usingawait using がスコープ化された宣言であることを理解しているかどうか。クリーンアップは、例外を含め、制御が囲んでいるブロックを離れるときに実行されます。トップレベルの長寿命スコープは、通常、不適切なライフタイムです。

第三に、破棄の順序を説明できるかどうか。リソースは宣言の逆順で破棄されるため、依存するリソースは依存先のリソースの後に宣言する必要があります。

第四に、エラーについて論理的に考えられるかどうか。本体のエラーと破棄のエラーの両方を、抑制されたエラーチェーンとして保持する必要がある場合があります。クリーンアップによってプライマリエラーが暗黙のうちに上書きされてはなりません。

第五に、互換性プランを提供できるかどうか。デプロイ先のランタイムがプロトコルを実装していない場合は、機能検出、コンパイラ変換、または明示的な try/finally アダプターが必要になることがあります。

最初に明確にすべき質問

  • どのNode.jsまたはブラウザのバージョンでコードが実行され、トランスパイルは許可されていますか?
  • どのリソースが同期的で、どのクリーンアップ操作がPromiseを返しますか?
  • キャンセルによってリソースは即座に閉じられますか、それとも進行中の作業を完了させることができますか?
  • リソースは独立していますか、それともあるリソースのクリーンアップが別のリソースが開いたままであることに依存していますか?
  • クリーンアップエラーはリクエストを失敗させる必要がありますか、報告される必要がありますか、それともプライマリエラーに関連付ける必要がありますか?
  • コードで DisposableStack を使用できますか、それとも基本的なシンボルのみですか?

30秒の回答フレームワーク

「各リソースに明示的な破棄プロトコルを与え、そのライフタイムを所有する最小のブロック内で宣言し、クリーンアップが非同期の場合は常に await using を使用します。逆順の破棄が安全に行われるように依存関係は後に宣言し、成功、本体の失敗、取得の失敗、キャンセルをテストし、本体とクリーンアップの両方のエラーを保持します。リリース前にはエンジンのサポートを確認するか、同等の try/finally にコンパイルします。TypeScriptでの構文サポートは、ランタイムサポートを保証するものではありません。」

ステップバイステップの回答

ステップ1:限定的な破棄プロトコルを定義する

取得とクリーンアップを一緒に保ちます。同期リソースは [Symbol.dispose]() を公開し、ブロックが終了する前にクリーンアップを完了する必要があります。非同期リソースは [Symbol.asyncDispose]() を公開し、生成された終了パスがそれをawaitするように await using で取得されます。

ts
class FileLease {
  constructor(private readonly fd: number) {}
  [Symbol.dispose]() { closeFile(this.fd) }
}

class AsyncLockLease {
  constructor(private readonly release: () => Promise<void>) {}
  async [Symbol.asyncDispose]() { await this.release() }
}

呼び出し元が明示的にキャンセルすることもできる場合、メソッドはべき等である必要があります。[Symbol.dispose]() からPromiseを返してはなりません。awaitされる作業には非同期プロトコルを使用してください。

ステップ2:ライフタイムブロックを小さく保つ

それを所有するスコープに入った後にのみリースを取得します。using 変数をより長寿命のオブジェクトに格納することは避けてください。そのクリーンアップはガベージコレクションではなく、レキシカルブロックに関連付けられています。

ts
async function handle() {
  {
    using file = openFileLease()
    await using lock = await acquireLockLease()
    await writeWithLock(file, lock)
  }
}

ここではロックがファイルの後に宣言されているため、ロックが最初に解放され、ファイルが2番目に閉じられます。本体がスローした場合でも、両方の終了処理が実行されます。

ステップ3:取得とキャンセルを処理する

リソースを順次取得するか、取得が成功したらすぐにスタックに登録します。後続の取得が失敗した場合でも、すでに取得されたリソースは破棄される必要があります。操作にアボートシグナルを接続しますが、キャンセルによってクリーンアップがバイパスされないようにスコープ内に破棄を保持します。

ステップ4:失敗情報を保持する

本体の例外とクリーンアップの例外を個別にテストし、次に両方を同時にテストします。ランタイムは、プライマリエラーによって抑制されたものとしてクリーンアップの失敗を表すことができます。ロギングには完全なチェーンを含める必要があります。アダプターが try/finally を使用している場合は、本体のエラーを上書きするのではなく、クリーンアップの失敗を明示的に添付します。

ステップ5:互換性を計画する

TypeScriptコンパイラだけでなく、実際のデプロイエンジンを確認してください。ネイティブ構文やシンボルが利用できない場合は、try/finally にコンパイルするか、検証済みのポリフィルを使用するか、アプリケーションレベルの破棄可能ヘルパーでリソースをラップします。フォールバックのセマンティクス(逆順、1回限りのクリーンアップ、awaitされる非同期解放、および保持されるエラー)は同一に保ちます。

ステップ6:ライフサイクルの境界をテストする

取得および破棄イベントを記録する決定論的なフェイクを使用します。正常なリターン、本体のスロー、2番目の取得の失敗、作業中のアボート、破棄の失敗、および繰り返される破棄をカバーします。イベントの順序と、Promiseが確定した後に開いたままのリソースがないことをアサートします。

模範解答

「各ハンドルを破棄可能なリースとしてモデル化します。同期ハンドルは [Symbol.dispose] を実装し、非同期解放は [Symbol.asyncDispose] を実装します。これらを所有する最小のブロック内で作成し、ロックには await using を使用し、逆順のクリーンアップが依存関係を尊重するようにロックをファイルの後に宣言します。スコープはリターン、スロー、キャンセル時に終了するため、言語プロトコルによってクリーンアップが保証されます。

取得の失敗、本体の失敗、クリーンアップの失敗、およびそれらの組み合わせをテストし、プライマリエラーに加えて抑制されたクリーンアップ情報を保持します。また、べき等性とアボートの動作も検証します。最後に、本番エンジンを確認します。TypeScript 5.2のサポートはコンパイル時の支援であり、ランタイムがシンボルを実装していることの証明ではありません。サポートがない場合は、トランスパイルするか、同じ順序とエラーセマンティクスを持つ明示的な try/finally アダプターを使用します。」

よくある間違い

  • リソースを長寿命のスコープに配置する → クリーンアップが遅延する → それを所有する最小のブロックにバインドする。
  • 非同期クリーンアップに using を使用する → Promiseが無視される可能性がある → [Symbol.asyncDispose] を実装し、await using と共に使用する。
  • 依存関係を先に宣言する → 逆順処理により早すぎる段階で閉じられる → 依存する側を後に宣言する。
  • TypeScriptのサポートがランタイムのサポートを意味すると仮定する → 本番環境でパースやシンボル検索に失敗する → エンジンを確認するかフォールバックへコンパイルする。
  • 本体のエラーをクリーンアップの失敗で上書きする → 根本原因が失われる → プライマリエラーと抑制されたクリーンアップの詳細を保持する。
  • 二重解放を許可する → クリーンアップが安全でなくなる → 破棄をべき等にするかガードする。
  • 成功のみをテストする → 失敗パスでリソースがリークする → 取得、本体、キャンセル、および破棄のエラーをテストする。

フォローアップの質問

フォローアップ1:using はガベージコレクションを置き換えますか?

いいえ。ハンドルやロックなどのリソースに対して決定論的なスコープベースのクリーンアップを提供しますが、メモリの回収は依然としてランタイムの役割です。

フォローアップ2:なぜ破棄は逆順なのですか?

後から宣言されたものは通常、先に宣言されたものに依存するためです。逆順にすることで、依存先が閉じる前に依存側を解放できます。

フォローアップ3:クリーンアップはいつ非同期にするべきですか?

フラッシュやリモートコーディネーターへのリースの返却など、解放自体にawaitされる操作が必要な場合に非同期プロトコルを使用します。

フォローアップ4:本体のエラーとクリーンアップのエラーの両方が発生した場合はどうなりますか?

本体のエラーをプライマリとして保持し、ランタイムの抑制されたエラーメカニズムまたは同等の明示的なエラーチェーンを通じてクリーンアップの失敗を公開します。

フォローアップ5:リソースを手動で破棄することもできますか?

はい、ただしスコープの終了によって2重に解放されないように、操作をべき等にするか所有権を調整してください。

フォローアップ6:ネイティブサポートがない場合のフォールバックは何ですか?

コンパイラ変換、検証済みのポリフィル、または逆順、await処理、1回限りのクリーンアップ、エラーチェーンを保持する小さな try/finally アダプターを使用します。

公開情報ソース

関連する質問

関連面接ツール

コーディング問題にはスクリーンショットを使用

問題をキャプチャし、制約条件、解法アプローチ、コード、エッジケース、計算量の順に進めます。

ツールを見る