Rustのdrop順序:ローカル変数、フィールド、一時値、そしてlet _がすぐdropされる理由

Rustではローカル変数は宣言の逆順、構造体のフィールドは宣言順にdropされます。一時値はふつう文の終わりで消え、let _は値を束縛しません。Rust 2024エディションのif letと末尾式の変更もリファレンスに沿って整理します。

rustdropraiirust-2024language-spec

Rustでは、ミューテックスのガード、ファイルハンドル、トランザクションのように「スコープを抜けるときに後始末をする」値の動作は、Dropがいつ実行されるかで決まります。たいていは直感どおりに動きます。ところがlet _ = mutex.lock().unwrap();と書いたその行でロックが解放されていたり、if letのelseブロックで同じロックを取り直そうとしてデッドロックしたりすることがあります。

この記事では、The Rust Referenceのdestructorsの章とRust 2024エディションガイドをもとに、drop順序の規則を整理します。難易度は中級で、所有権とDropトレイトは知っている前提です。

基本の規則

リファレンスの規則をまとめると次のとおりです。

  • すべての変数と一時値はdropスコープに属します。制御フローがそのスコープを出ると、そのスコープの変数は宣言の逆順に、一時値は生成の逆順にdropされます。
  • letで宣言したローカル変数は、そのletを含むブロックのスコープに属します。
  • 関数の引数は関数本体全体のスコープに属するので、関数が終わるときに最後にdropされます。
  • 値を1つdropするときは、その型がDropを実装していればまずDrop::dropを呼び、そのあとフィールドを再帰的にdropします。構造体のフィールドは宣言順、タプルの要素は順番どおり、配列や所有するスライスの要素は先頭から末尾へdropされます。
  • 1つのパターンの中で束縛された変数は、パターン内での宣言の逆順にdropされます。

ローカル変数は逆順、フィールドは正順です。向きが逆だという点を押さえておけば、残りのほとんどはここから導けます。

let _は値を束縛しない

リファレンスのパターンの章は、ワイルドカードパターン_について「識別子パターンと違い、マッチした値をコピーも移動も借用もしない」と書いています。一方、_cのようにアンダースコアで始まる名前はふつうの識別子パターンで、実際に変数を束縛します。値がCopy型ならコピーし、そうでなければ移動します。後の例のNoisyはCopyではないので、値が移動します。見た目は似ていても動作は違います。

ここに一時値の規則が加わります。let文の初期化式はプレース式の文脈で、関数呼び出しのような値式がそこに来ると名前のない一時領域が作られます。この一時値のdropスコープはふつうその文の終わりです。let x = &temp();のように参照で束縛すると一時値の寿命はブロックの終わりまで延長されますが、_は何も束縛しないので延長されません。

次のコードで確かめます。

struct Noisy(&'static str);

impl Drop for Noisy {
    fn drop(&mut self) {
        println!("drop {}", self.0);
    }
}

struct Pair {
    first: Noisy,
    second: Noisy,
}

fn main() {
    let a = Noisy("a");
    let _ = Noisy("b");
    let _c = Noisy("c");
    let pair = Pair { first: Noisy("pair.first"), second: Noisy("pair.second") };
    let _ = a;
    println!("end of main");
}

以下の出力はリファレンスの規則から導いたもので、この記事を書いた環境で実際に実行した結果ではありません。

drop b
end of main
drop pair.first
drop pair.second
drop c
drop a
  • let _ = Noisy("b");のNoisy("b")は一時値で、_に束縛されないので、そのlet文が終わった時点でdropされます。end of mainより先に出力されるのはこのためです。
  • let _ = a;はaを移動しません。この行では何も起きず、aはそのまま残ってmainの終わりにdropされます。
  • mainの終わりでは、変数が宣言の逆順のpair、_c、aの順にdropされます。
  • pairをdropするときは、フィールドが宣言順にfirst、secondの順でdropされます。

ロックで問題になる場合

rustcのlintドキュメントには次の例が載っています。

let _ = data.lock().unwrap();
println!("doing some work");

data.lock().unwrap()が返すMutexGuardは一時値で、_に束縛されないので、この文が終わるとすぐdropされます。つまりロックはその場で解放され、次の行はロックなしで実行されます。rustcはこれをlet_underscore_lock lintで検出し、2026-10-14に確認したlint一覧ではデフォルトがdenyなのでコンパイルエラーになります。

ただし、このlintが対象にするのは同期用のロックだけです。自作のガード型のようにDropを実装したほかの値に対応するlet_underscore_drop lintはデフォルトがallowなので、有効にしない限り警告なしで通ります。lintドキュメントが勧める方法は2つです。

  • スコープの終わりまで保持したいなら、let _guard = ...のようにアンダースコアで始まる名前に束縛します。
  • 本当にすぐ捨てたいなら、drop(...)を呼んで意図を明示します。

std::mem::dropは特別な仕組みではなく、pub fn drop<T>(_x: T) {}と定義されたふつうの関数です。値を引数として移動で受け取り、関数が終わるときにその値がdropされます。そのためCopy型に使うとコピーが渡されるだけで、元の値には影響しません。

一時値のスコープ

寿命の延長がない場合、一時値のdropスコープは、その式を含む次のスコープのうち最も小さいものです。リファレンスの一覧から、よく出会うものだけを挙げます。

  • 1つの文
  • if、while、loopの本体と、ifのelseブロック
  • ifやwhileの通常の(パターンマッチではない)条件式
  • &&と||の各オペランド
  • matchアームの本体
  • ブロックの末尾式全体(2024エディション)
  • if letの条件とthenブロック(2024エディション)

matchの検査対象の式(scrutinee)はこの一覧にありません。リファレンスはこれを明記していて、その結果scrutineeで作られた一時値はmatch式のあと、ふつうは文の終わりまで生きています。たとえばmatch *value.read().unwrap() { ... }と書くと、どのアームを実行している間も読み取りロックは保持されたままです。これはエディションに関係なく同じです。

Rust 2024エディションで変わった2つの点

エディションガイドによると、Rust 2024エディションはRust 1.85.0で安定化され、一時値のスコープを狭める規則が2つ追加されました。エディションはクレート単位の設定で、CargoではCargo.tomlの[package]にあるeditionキーで指定します。Cargoのドキュメントには、このキーがない場合は2015エディションとみなすと書かれています。

if letの一時値はelseの前にdropされる

エディションガイドの例を引用します。

use std::sync::RwLock;

fn f(value: &RwLock<Option<bool>>) {
    if let Some(x) = *value.read().unwrap() {
        println!("value is {x}");
    } else {
        let mut v = value.write().unwrap();
        if v.is_none() {
            *v = Some(true);
        }
    }
}

2021エディションまでは、value.read()が作った読み取りロックの一時値がif let式全体の終わり、つまりelseブロックのあとまで生きていました。そのためelseに入って書き込みロックを取ろうとするとデッドロックします。2024エディションでは、thenブロックが終わるか制御がelseに移った時点で一時値がdropされるので、このコードは意図どおりに動きます。

逆に、以前のように一時値をelseの先まで保持することを前提にしたコードもありえます。エディションガイドは、if letをmatchに書き換えるのは常に安全だと説明しています。matchのscrutineeの一時値は先に見たようにmatch式のあとまで生きるので、2021エディションのif letと同じ動作になります。cargo fix --editionはif_let_rescope lintを通じてこのmatchへの書き換えを提案しますが、ガイドは適用後に各変更が本当に必要かを見直すよう勧めています。上の例をmatchに書き換えると、デッドロックが戻ってきます。

末尾式の一時値はローカル変数より先にdropされる

ブロックや関数本体の最後の式(末尾式)で作られた一時値は、2021エディションまではブロックのローカル変数よりあとにdropされていました。そのため次のコードは2021エディションではcの寿命が足りないというE0597エラーになります。

use std::cell::RefCell;

fn f() -> usize {
    let c = RefCell::new("..");
    c.borrow().len()
}

2024エディションではc.borrow()が作った一時値が先にdropされ、そのあとcがdropされるので、コンパイルが通ります。

この変更によって、逆に2024エディションでコンパイルできなくなるコードもあります。エディションガイドの例let x = { &String::from("1234") }.len();は、2021エディションでは一時的なStringが文の終わりまで延長されていましたが、2024エディションではブロックの終わりでdropされるので、借用中にdropされるというエラーになります。ガイドが示す対処は、let s = { &String::from("1234") };のようにブロックの結果をまずローカル変数に束縛し、寿命の延長を受けることです。

末尾式の変更には、意味を保ったまま自動で書き換える方法がありません。tail_expr_drop_order lintは影響を受けうる値を知らせるだけでコードは変えないので、警告を読んで自分で判断する必要があります。

まとめ

  • 同じブロックのローカル変数は宣言の逆順、構造体のフィールドは宣言順にdropされます。ほかの言語で似た規則を見たことがあれば、Goのdeferが登録の逆順に実行され、Javaのtry-with-resourcesがリソースを宣言の逆順に閉じるのと同じ向きです。
  • let _ = expr;は値を束縛しません。exprが一時値なら文の終わりですぐdropされ、既存の変数なら移動しません。スコープの終わりまで保持したいなら_guardのような名前を使います。
  • matchのscrutineeの一時値はmatchのあとまで生きています。
  • 2024エディションでは、if letの一時値はelseの前に、末尾式の一時値はローカル変数より先にdropされます。エディションを上げるときは、ロックやRefCellの借用が絡むコードについて、この2つのlintの警告を確認しておくとよいでしょう。

範囲と出典

この記事は2026-10-14に確認したThe Rust ReferenceとRust Edition Guideの内容にもとづいています。確認したリファレンスのページにはバージョン表記がなく、destructorsのページ内のテストへのリンクはrust-lang/rustの1.99.0タグを指していました。リファレンスは、一時値の寿命延長の正確な規則は変わる可能性があり、現在の動作を説明したものだと断っています。lintのデフォルトレベル(deny/allow)も、確認時点のrustcドキュメントによるものです。

パニック時のアンワインド中のdrop、クロージャが移動でキャプチャした変数のdrop順序(リファレンス上は未規定)、ManuallyDropとmem::forget、asyncとの関係は扱いません。例の出力は規則から導いたもので、実行結果ではありません。

以下のリンクは2026-10-14に確認しました。

ブログへ