Drop Bomb: API の正しさを強制する

不変条件を強制し、API の誤った使い方を検出するには Drop を使います。 「drop bomb」は、値が明示的にファイナライズされないままドロップされると panic します。

このパターンは、ファイナライズ操作(commit()rollback() など)が Result を返す必要がある場合によく使われます。これは Drop からは 行えません。

// Copyright 2025 Google LLC
// SPDX-License-Identifier: Apache-2.0

use std::io::{self, Write};

struct Transaction {
    active: bool,
}

impl Transaction {
    fn start() -> Self {
        Self { active: true }
    }

    fn commit(mut self) -> io::Result<()> {
        writeln!(io::stdout(), "COMMIT")?;
        self.active = false;
        Ok(())
    }
}

impl Drop for Transaction {
    fn drop(&mut self) {
        if self.active {
            panic!("Transaction dropped without commit!");
        }
    }
}

fn main() -> io::Result<()> {
    let tx = Transaction::start();
    // `tx` を使ってトランザクションを構築し、その後コミットします。
    // panic を確認するには、`commit` の呼び出しをコメントアウトしてください。
    tx.commit()?;
    Ok(())
}
  • 一部のシステムでは、値はドロップされる前に特定の API によって ファイナライズされなければなりません。

    たとえば、Transaction はコミットまたはロールバックする必要がある 場合があります。

  • drop bomb は、Transaction のような値が未完了の状態のまま黙って ドロップされないことを保証します。トランザクションが明示的に ファイナライズされていない場合(たとえば commit() していない場合)、 デストラクタは panic します。

  • ファイナライズ操作(commit() など)は通常、self を値で受け取ります。 これにより、トランザクションがファイナライズされた後は、元のオブジェクトを もう使えなくなります。

  • このパターンを使う一般的な理由は、クリーンアップを Drop で行えない 場合です。失敗する可能性がある、または非同期であることがその理由です。

  • このパターンは公開 API でも適切です。トランザクションオブジェクトを 明示的にファイナライズし忘れたときに、ユーザーが早い段階でバグを 見つける助けになります。

  • クリーンアップを Drop で安全に行えるなら、デバッグビルドでのみ panic することを選ぶ API もあります。これが適切かどうかは、API が強制しなければ ならない保証に依存します。

  • 誤用が黙って見過ごされることで重大な正しさやセキュリティ上の問題が 生じる場合、リリースビルドで panic するのは妥当です。

  • 質問: なぜ Transaction の中に active フラグが必要なのでしょうか。なぜ drop() は無条件に panic してはいけないのでしょうか。

    期待される答え: commit()self を値で受け取り、その際に drop() が 実行されるため、panic してしまいます。

さらに調べる

適切な後始末を強制したり、誤ってドロップしてしまうことを防いだりする 関連パターンがいくつかあります。

  • drop_bomb crate: .defuse() で明示的に無効化しない限り、ドロップされると panic する小さな ユーティリティです。デバッグビルドでのみ有効になる DebugDropBomb バリアントも付属しています。