3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

自作catコマンドで学ぶ"?"について

Last updated at Posted at 2025-12-18

この記事はRust Advent Calendar 2025 シリーズ2 19日目の記事です。

初めに

お久しぶりです、私です。Twil3akineと申すものです。
Rustについて久々に書きます。

前回はRustの所有権についてふわっと書きましたが、今回は?の扱い方について、書いていこうと思ってます。

環境

以下のバージョンを使用しています。

  • Windows11 Home
  • rustc 1.91.1
  • cargo 1.91.1

Rustにおける?とは

Docを読む

The Rust Programming Languageによると、

演算子 説明
? expr? エラー委譲

と書いてあるように、?は「エラー委譲」、つまり 「成功したら中身を取り出す、失敗したら即座にエラーを返す」 を行うシンタックスシュガーということです。

具体例

例えば、File::openを真面目にエラーハンドリングすると、

pub fn open<P: AsRef<Path>>(path: P) -> Result<File>

とあるように、Result型を返すので

let file = match File::open("foo.txt") {
    Ok(f) => f, // 成功したら中身を取り出す
    Err(e) => return Err(e), // 失敗したら、エラーをそのまま返す
};

もしくは、

let file = File::open("foo.txt").expect("Failed open file.");

という風になると思いますが、正直めんどくさいと思います。
そこで、?を用いることで以下のようにすっきりします。

let file = File::open("foo.txt")?;

すっきりしましたね。

本題

題材

ということで、そろそろ本題の簡易的な自作catコマンドのコードについて考えます。

use std::env;
use std::fs::File;
use std::io::Read;
use std::process;

fn main() {
    // コマンドライン引数を受け取る
    let args: Vec<String> = env::args().collect();

    // 引数がなかったら異常終了(exit code: 1)として返す
    if args.len() < 2 {
        eprintln!("Usage: {} <filename>", args[0]);
        process::exit(1);
    }

    // コマンドライン引数からファイル名を受け取る
    let file_name = &args[1];

    // 受け取ったファイル名からファイルオブジェクトを作成
    let mut file = File::open(file_name).expect("file not found.");

    // 文字列を作成し、その中に内容を入れていく
    let mut contents = String::new();
    file.read_to_string(&mut contents)
        .expect("something went wrong reading the file.");

    // 出力する
    println!("{}", contents);
}
message.txt
Hi!
I am a Rust beginner!

実行すると、こんな感じになります。

$ cargo run message.txt

Hi!
I am a Rust beginner!


$ cargo run

Usage: cat_command.exe <filename>
error: process did not exit successfully: `cat_command.exe` (exit code: 1)

このコードの嬉しくないところ

  1. ファイルが存在しないとパニックしてしまう
    expectは便利だが、処理に失敗すると即座にパニックを発生させてしまうため、使う立場からすると、エラーログを残したまま急に止まるので、あまり行儀の良いツールとは言えない

  2. エラーハンドリングの責任を放棄している
    expectだと、エラーが起きたその場で問答無用にプログラムが終わってしまうので、
    もし将来、「ファイルがなかったらデフォルト値を使う」みたいな処理を入れたくなった時、expectだとそこで試合終了、復帰ができない

  3. どうせなら、もっとRustっぽく書きたい

などがあると思います。

.expect()?に置き換えてみる

では、?を使ったコードに変えていきましょう。
さっき具体例を見た感じ「.expect()?に置き換えたらいいんじゃないか」と思うので、やってみましょう。

use std::env;
use std::fs::File;
use std::io::Read;
use std::process;

fn main() {
    // コマンドライン引数を受け取る
    let args: Vec<String> = env::args().collect();

    // 引数がなかったら異常終了(exit code: 1)として返す
    if args.len() < 2 {
        eprintln!("Usage: {} <filename>", args[0]);
        process::exit(1);
    }

    // コマンドライン引数からファイル名を受け取る
    let file_name = &args[1];

    // 受け取ったファイル名からファイルオブジェクトを作成
-   let mut file = File::open(file_name).expect("file not found.");
+   let mut file = File::open(file_name)?;

    // 文字列を作成し、その中に内容を入れていく
    let mut contents = String::new();
-   file.read_to_string(&mut contents)
-        .expect("something went wrong reading the file.");
+   file.read_to_string(&mut contents)?;

    // 出力する
    println!("{}", contents);
}

実行してみましょう。

$ cargo run message.txt

error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
  --> src\main.rs:20:41
   |
 6 | fn main() {
   | --------- this function should return `Result` or `Option` to accept `?`
...
20 |     let mut file = File::open(file_name)?;
   |                                         ^ cannot use the `?` operator in a function that returns `()`
   |
help: consider adding return type
   |
 6 ~ fn main() -> Result<(), Box<dyn std::error::Error>> {
 7 |     // コマンドライン引数を受け取る
...
26 |     println!("{}", contents);
27 +     Ok(())
   |

error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
  --> src\main.rs:24:39
   |
 6 | fn main() {
   | --------- this function should return `Result` or `Option` to accept `?`
...
24 |     file.read_to_string(&mut contents)?;
   |                                       ^ cannot use the `?` operator in a function that returns `()`
   |
help: consider adding return type
   |
 6 ~ fn main() -> Result<(), Box<dyn std::error::Error>> {
 7 |     // コマンドライン引数を受け取る
...
26 |     println!("{}", contents);
27 +     Ok(())
   |

For more information about this error, try `rustc --explain E0277`.
error: could not compile `cat_command` (bin "cat_command") due to 2 previous errors

なんかエラーメッセージが出ましたね。
エラーメッセージを読んだ感じ、関数の返り値にResult型かOption型を返さないといけない感じがしますね。

Result型を返すように変更する

コンパイラのエラーメッセージ通りに修正してみましょう。

use std::env;
+ use std::error::Error;
use std::fs::File;
use std::io::Read;
use std::process;

- fn main() {
+ fn main() -> Result<(), Box<dyn Error>> {
    // コマンドライン引数を受け取る
    let args: Vec<String> = env::args().collect();

    // 引数がなかったら異常終了(exit code: 1)として返す
    if args.len() < 2 {
        eprintln!("Usage: {} <filename>", args[0]);
        process::exit(1);
    }

    // コマンドライン引数からファイル名を受け取る
    let file_name = &args[1];

    // 受け取ったファイル名からファイルオブジェクトを作成
    let mut file = File::open(file_name)?;

    // 文字列を作成し、その中に内容を入れていく
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;

    // 出力する
    println!("{}", contents);

+   Ok(())
}

とりあえず、実行してみましょう。

$ cargo run message.txt

Hi!
I am a Rust beginner!


$ cargo run

Usage: cat_command.exe <filename>
error: process did not exit successfully: `cat_command.exe` (exit code: 1)

無事に実行できていることが確認できました。

なぜコンパイルエラーが発生したのか

コンパイラのエラーメッセージにもあった通り、Result型を返す関数内でしか?は使えないようです。

なので、

- fn main() {
+ fn main() -> Result<T, E> {
    ...
    // Ok<T>型を返す
    Ok(T)
}

こういう感じでmain関数がResult型を返すようにしないといけなく、今回は返す値がないので、Tには(), Eにはstd::error::Errorが入り、以下のようになるはずと思うのですが、

fn main() -> Result<(), Error> {
    ...
    // 今回はT=()なのでOk<()>型を返す
    Ok(())
}

実際のコードを見ると、Result<(), Box<dyn std::error::Error>>となっています。
Box<dyn std::error::Error>とはいったい何なのでしょうか。

The Rust Programming Languageには、以下のように書かれています。

The Box<dyn Error> type is a trait object, which we'll talk about in "Using Trait Objects to Abstract over Shared Behavior" in Chapter 18. For now, you can read Box<dyn Error> to mean "any kind of error." Using ? on a Result value in a main function with the error type Box<dyn Error> is allowed because it allows any Error value to be returned early.

(中略)

When a main function returns a Result<(), E>, the executable will exit with a value of 0 if main returns Ok(()) and will exit with a nonzero value if main returns an Err value.

要約すると、

  1. 「あらゆる種類のエラー」を扱える
    Box<dyn Error>は「どんな種類のエラーでも入れられる魔法の箱1」のような扱いができ、これでファイル読み込みエラーでも、パースエラーでもなんでも返すことができる。
  2. 終了コードが適切になる
    正常終了(Ok)のときは0、エラー(Err)のときは0以外のステータスコードを返して、プログラムが終了することができる。

と、言う感じらしいです。

これで?を使い方を完全に理解した完全体になりました。あなたは。おめでとうございます。

触れなかったところ

本題で触れなかった?Optionで返すパターンにも軽く触れておきます。
Result<T, E>に対して、Option<T>となるので、Result型のように返り値の型に気をもむ必要はありません。うれしいですね。

以下は配列内に指定した要素があるかないかを返すコードです。

use std::env;
use std::process;

/// 探したい人をDBに問いかけて、いたら情報を返す
fn find_user(target_name: &str) -> Option<(i32, &'static str)> {
    // DB(今回は代役)
    let users = vec![(0, "Alice"), (1, "Bob"), (2, "Charlie")];

    // 探したい人がいたら返却、いなかったらNone (?で脱出)
    let user = users.iter().find(|&&(_id, name)| name == target_name)?;

    // 見つかったらSomeで包んで返す
    Some(*user)
}

// main関数でOptionを返すことができない
fn main() {
    // コマンドライン引数を受け取る
    let args: Vec<String> = env::args().collect();

    // 引数がなかったら異常終了(exit code: 1)として返す
    if args.len() < 2 {
        eprintln!("Usage: {} <filename>", args[0]);
        process::exit(1);
    }

    // コマンドライン引数からファイル名を受け取る
    let target_name = &args[1];

    // 関数を呼び出す
    match find_user(target_name) {
        Some(user) => println!("Found: ID={} Name={}", user.0, user.1),
        None => println!("User not found."),
    };
}

まとめ

  • ?を用いることで、見るべきところを見やすくなる
  • ?を用いる時は、使用している関数の返り値の型をOption<T>, Result<T, E>にする。また、Result<T, E>を使用するときはEの型に気を付ける
  • Rustは神
  1. 魔法の正体はFromトレイト
    ?は、エラーを返すだけではなく、「関数の戻り値の型に合わせて、エラーの型を自動変換する」というFromトレイトのfrom関数呼び出しを行っている
    今回の場合では、File::openが返すstd::io::Errorは、Fromが実装済みなので、?だけで自動でエラーを返すことができる。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?