Rustというプログラミング言語を学んでいると、コードの至る所でアンダースコア(_)という記号を見かける機会が多いのではないでしょうか。

Rustにおけるアンダースコアは、一見すると単なる記号のように思えるかもしれません。

しかし、この記号はコンパイラに対して「特定の値を無視する」という明確な意思を伝えるための重要な役割を担っています。

アンダースコアを適切に使いこなすことは、コードの可読性を高めるだけでなく、コンパイラの警告を適切に制御し、意図しないバグを防ぐことにも繋がります。

本記事では、Rustにおけるアンダースコアの多様な意味と、変数、パターンマッチング、型推論、そして数値リテラルにおける具体的な使い方について詳しく整理していきます。

変数宣言におけるアンダースコアの役割

Rustで変数を宣言したものの、その値を一度も使わない場合、コンパイラは「未使用の変数」として警告を出します。

開発の途中段階や、インターフェースの都合上どうしても引数を受け取る必要がある場合には、この警告が煩わしく感じることがあるでしょう。

このような場面で、アンダースコアが非常に役立ちます。

未使用変数への警告を抑制する

変数の名前の先頭にアンダースコアを付けることで、その変数が意図的に使われていないことをコンパイラに伝えることができます。

例えば、_count のように記述します。

これにより、コンパイラは「この変数は将来使う予定があるか、あるいは意図的に残されているのだな」と判断し、警告を出さなくなります。

Rust
fn main() {
    // コンパイラは警告を出さない
    let _unused_variable = 10;
    
    println!("プログラムは正常に動作しています");
}
実行結果
プログラムは正常に動作しています

値を即座に破棄する純粋なアンダースコア

名前を付けずに let _ = ... と記述した場合、その値は即座にドロップ(破棄)されます

これは let _x = ... とは挙動が異なる重要なポイントです。

_x のように名前を付けた場合は、その変数がスコープを抜けるまで値が保持されます。

一方で、単なる _ は、値を束縛(バインド)せずにその場で捨て去ることを意味します。

この違いは、ミューテックス(Mutex)のロック解除タイミングなど、メモリ管理やリソース解放が重要になる場面で大きな差を生みます。

Rust
struct Droppable;

impl Drop for Droppable {
    fn drop(&mut self) {
        println!("破棄されました!");
    }
}

fn main() {
    println!("開始");
    let _ = Droppable; // ここで即座にdropが呼ばれる
    println!("終了");
}
実行結果
開始
破棄されました!
終了

パターンマッチングにおけるワイルドカード

Rustの最も強力な機能の一つである match 式において、アンダースコアは「ワイルドカード」として機能します。

これは、他のどのパターンにも一致しなかった場合に実行される「その他すべて」を表現するために使われます。

網羅性のチェックとアンダースコア

Rustの match 式は網羅的(Exhaustive)でなければなりません。

つまり、起こり得るすべてのケースを網羅している必要があります。

しかし、すべてのパターンを個別に記述するのが現実的でない場合、アンダースコアを最後のパターンとして置くことで、残りのケースを一括して処理できます

Rust
fn main() {
    let some_value = 7;

    match some_value {
        1 => println!("1です"),
        2 => println!("2です"),
        3 => println!("3です"),
        _ => println!("それ以外の数値です"), // 1, 2, 3 以外はすべてここ
    }
}
実行結果
それ以外の数値です

列挙型(Enum)の一部を無視する

列挙型の中に複数のデータが含まれている場合、その一部だけが必要で、残りは無視したいことがあります。

そのような場合も、アンダースコアを使って特定のフィールドだけをスキップできます。

これは if letwhile let でも同様に利用可能です。

Rust
enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
}

fn main() {
    let msg = Message::Move { x: 10, y: 20 };

    match msg {
        Message::Move { x, y: _ } => {
            // yの値は無視してxだけを使う
            println!("xの座標は {} です", x);
        },
        _ => (), // 他のパターンは何もしない
    }
}
実行結果
xの座標は 10 です

型推論におけるプレースホルダー

Rustは強力な型推論を持っていますが、時としてコンパイラに「ここに入る型は文脈から判断してほしい」とヒントを与える必要があります。

この場合、型名の一部として _ を使用することができます。

ジェネリクスでの活用

例えば、collect メソッドを使用してイテレータをコレクションに変換する場合、どのコレクションに変換するかを指定する必要があります。

しかし、コレクションの中身の型(要素の型)まで明示するのは冗長な場合があります。

Vec<_> と記述することで、要素の型はコンパイラの推論に任せることが可能になります

Rust
fn main() {
    let numbers = vec![1, 2, 3, 4, 5];

    // 要素の型を i32 と書かずに _ で済ませる
    let doubled: Vec<_> = numbers.iter().map(|x| x * 2).collect();

    println!("{:?}", doubled);
}
実行結果
[2, 4, 6, 8, 10]

このように記述することで、将来的に numbers の型が i64f64 に変更されたとしても、doubled の型定義を修正する必要がなくなります。

Turbofish演算子との併用

型推論を補助する Turbofish(::<>)においてもアンダースコアは有効です。

HashMap のように複数の型パラメータを持つ場合、一方の型だけを明示し、もう一方は推論させるという使い方が一般的です。

Rust
use std::collections::HashMap;

fn main() {
    // キーの型はStringと明示し、値の型は推論させる
    let mut scores = HashMap::<String, _>::new();
    scores.insert(String::from("Blue"), 10);
    
    println!("{:?}", scores);
}

その他の特殊なアンダースコアの用途

変数名や型推論以外にも、Rustにはアンダースコアが登場する場面があります。

これらはコードの可読性やメンテナンス性を向上させるために設計されています。

数値リテラルの区切り文字

桁数の多い数値を記述する際、人間にとって読みやすくするためにアンダースコアを挿入することができます。

これはコンパイラにとっては無視される存在であり、純粋に視覚的な補助としての機能です。

1_000_0001000000 は、プログラム上では全く同じ意味を持ちます。

Rust
fn main() {
    let big_number = 1_000_000_000;
    let hex_bytes = 0xFF_AA_11;
    let binary_val = 0b1111_0000;

    println!("数値: {}", big_number);
    println!("16進数: {:X}", hex_bytes);
    println!("2進数: {:b}", binary_val);
}
実行結果
数値: 1000000000
16進数: FFAA11
2進数: 11110000

関数やメソッドの未使用パラメータ

トレイトを実装する際、トレイトの定義上は引数が必要であっても、特定の実装ではその引数を使わないことがあります。

このような場合、引数名として _ を使用することで、その引数が使われないことを明示できます。

Rust
trait Processor {
    fn process(&self, data: String);
}

struct Logger;

impl Processor for Logger {
    // dataは使わないが、シグネチャを合わせる必要がある
    fn process(&self, _: String) {
        println!("ログを記録しましたが、データの内容は無視しました");
    }
}

fn main() {
    let logger = Logger;
    logger.process(String::from("秘密のデータ"));
}

モジュールのインポートにおけるアンダースコア

特定のトレイトが持つメソッドを使いたいものの、そのトレイト名をコード内で直接参照する予定がない場合、use path::to::Trait as _; と記述することがあります。

これにより、名前空間を汚染することなく、トレイトのメソッド(拡張メソッドのようなもの)だけを有効化できます。

アンダースコア使用時の注意点

アンダースコアは非常に便利ですが、使い所を間違えると意図しない挙動を招く恐れがあります。

特に、前述した「即時ドロップ」の性質には注意が必要です。

記述形式意味所有権・生存期間
let x = ...変数xに束縛するスコープ終了時にドロップ
let _x = ...変数_xに束縛する(未使用警告なし)スコープ終了時にドロップ
let _ = ...値を無視する(束縛しない)即座にドロップ

例えば、一時的なガードオブジェクト(ロックなど)を let _ = lock.lock(); と記述してしまうと、ロックを取得した瞬間に解放されてしまいます。

クリティカルセクションを保護したい場合は、必ず let _guard = ... のように名前(アンダースコア付きの名前)を付けて、スコープの終わりまで生存期間を延ばす必要があります。

また、match 式での _ (ワイルドカード)の多用にも慎重になるべきです。

新しい列挙型のバリアントを追加した際、_ を使っているとコンパイラがエラーを出してくれないため、実装漏れに気づきにくくなるリスクがあります。

可能な限り具体的なパターンを記述し、どうしても必要な場合に限って _ を使用するのがRustらしい安全なコーディングと言えるでしょう。

まとめ

Rustにおけるアンダースコア(_)は、単なる「無視」以上の意味を持つ多機能なツールです。

未使用変数の警告抑制、パターンマッチングのワイルドカード、型推論の補助、そしてリソース管理における即時ドロップの制御など、その用途は多岐にわたります。

これらを正しく理解し使い分けることで、Rustコンパイラとより深く対話し、クリーンで安全なコードを記述できるようになります。

まずは、数値リテラルの区切りや、未使用引数の整理といった簡単な部分から取り入れてみてください。

アンダースコアの挙動をマスターすることは、Rustプログラマとしてのステップアップに不可欠な要素と言えるでしょう。