この記事は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);
}
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)
このコードの嬉しくないところ
-
ファイルが存在しないとパニックしてしまう
expectは便利だが、処理に失敗すると即座にパニックを発生させてしまうため、使う立場からすると、エラーログを残したまま急に止まるので、あまり行儀の良いツールとは言えない -
エラーハンドリングの責任を放棄している
expectだと、エラーが起きたその場で問答無用にプログラムが終わってしまうので、
もし将来、「ファイルがなかったらデフォルト値を使う」みたいな処理を入れたくなった時、expectだとそこで試合終了、復帰ができない -
どうせなら、もっと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 readBox<dyn Error>to mean "any kind of error." Using?on aResultvalue in amainfunction with the error typeBox<dyn Error>is allowed because it allows anyErrorvalue to be returned early.(中略)
When a
mainfunction returns aResult<(), E>, the executable will exit with a value of 0 ifmainreturnsOk(())and will exit with a nonzero value ifmainreturns anErrvalue.
要約すると、
- 「あらゆる種類のエラー」を扱える
Box<dyn Error>は「どんな種類のエラーでも入れられる魔法の箱1」のような扱いができ、これでファイル読み込みエラーでも、パースエラーでもなんでも返すことができる。 - 終了コードが適切になる
正常終了(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は神
-
魔法の正体は
Fromトレイト
?は、エラーを返すだけではなく、「関数の戻り値の型に合わせて、エラーの型を自動変換する」というFromトレイトのfrom関数呼び出しを行っている
今回の場合では、File::openが返すstd::io::Errorは、Fromが実装済みなので、?だけで自動でエラーを返すことができる。 ↩