状況の説明
ファイルI/Oのコードをリファクタリング中に、冗長な.unwrap()を省こうとFile::open()の呼び出しに?を付けた途端、Rustが即座にエラーを返します:
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:3:29
|
3 | let f = File::open("data.txt")?;
| ^ cannot use the `?` operator in a function that returns `()`
|
= help: the trait `FromResidual<Result<Infallible, std::io::Error>>` is not implemented for `()`
この関数の戻り値は()(つまり何も返しません)。?演算子はエラーを伝播させる先が必要ですが、Resultがないため伝播先が存在しません。
なぜこのエラーが起きるのか
?演算子はエラー発生時に早期リターンを行うシンタックスシュガーです。some_fn()?は内部的に次のように展開されます:
match some_fn() {
Ok(val) => val,
Err(e) => return Err(e.into()), // 早期リターン
}
このreturn Err(e)は、関数がResultを返すことを前提としています。Resultを返さない関数では有効なリターンパスが存在しません。同じ理屈がOptionにも当てはまります:Optionに対して?を使うとNoneを早期リターンするため、関数もOptionを返す必要があります。
修正方法1:main()の戻り値をResultに変更する
このエラーが最も頻繁に発生するのはmain()です。戻り値の型を追加し、末尾にOk(())を加えましょう:
use std::fs::File;
use std::io::Read;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut f = File::open("data.txt")?;
let mut contents = String::new();
f.read_to_string(&mut contents)?;
println!("{}", contents);
Ok(())
}
Box<dyn std::error::Error>は万能な型で、Errorトレイトを実装するあらゆるエラーを受け付けます。ほとんどのバイナリや使い捨てスクリプトには十分です。
anyhowクレートを使うと、さらにシンプルに書けます:
use anyhow::Result;
fn main() -> Result<()> {
let contents = std::fs::read_to_string("data.txt")?;
println!("{}", contents);
Ok(())
}
修正方法2:通常の関数の戻り値の型を変更する
どんな関数でも?を使えます。ただし、適切な戻り値の型が必要です。修正前後を比較します:
// 修正前 — コンパイルエラー
fn load_config() {
let contents = std::fs::read_to_string("config.toml")?;
// ...
}
// 修正後 — コンパイル成功
fn load_config() -> Result<String, std::io::Error> {
let contents = std::fs::read_to_string("config.toml")?;
Ok(contents)
}
一つの関数で複数のエラー型を扱う場合、ボックス化されたエラーを使うと型変換を手書きしなくて済みます:
fn load_and_parse() -> Result<MyConfig, Box<dyn std::error::Error>> {
let contents = std::fs::read_to_string("config.toml")?;
let config: MyConfig = toml::from_str(&contents)?; // 異なるエラー型も問題なし
Ok(config)
}
修正方法3:クロージャ内で?を使う
クロージャの場合はやや複雑です。次のコードはコンパイルできません:
let results: Vec<_> = paths.iter().map(|p| {
let content = std::fs::read_to_string(p)?; // ここでエラー
content
}).collect();
クロージャの推論された戻り値の型はStringであり、Result<String, _>ではありません。クロージャが明示的にResultを返すようにし、Result<Vec>にcollectします。最初のエラー発生時点で処理が止まります:
let results: Result<Vec<_>, _> = paths.iter().map(|p| {
std::fs::read_to_string(p)
}).collect();
match results {
Ok(contents) => { /* 全ファイルの読み込み成功 */ }
Err(e) => eprintln!("失敗: {}", e),
}
エラーを無視してスキップしたい場合は、.ok()とfilter_mapを組み合わせます:
let contents: Vec<String> = paths.iter()
.filter_map(|p| std::fs::read_to_string(p).ok())
.collect();
修正方法4:ResultとOptionを混在させる場合
ResultとOptionを一つの関数内で混在させると躓きやすいです。この2つは異なる型であり、変換なしに両方に?を使うことはできません:
use std::collections::HashMap;
// エラー — 関数はResultを返すが、?をOptionに対して使用している
fn get_value(map: &HashMap<String, String>, key: &str) -> Result<String, String> {
let val = map.get(key)?; // OptionであってResultではない!
Ok(val.clone())
}
.ok_or()を使ってOptionをResultに変換します:
fn get_value(map: &HashMap<String, String>, key: &str) -> Result<String, String> {
let val = map.get(key).ok_or_else(|| format!("キー'{}'が見つかりません", key))?;
Ok(val.clone())
}
逆のパターン(関数がOptionを返すがResultがある場合)は、.ok()でエラーを捨てます:
fn try_read(path: &str) -> Option<String> {
std::fs::read_to_string(path).ok()
}
修正の確認
フルビルドの前に、まず高速な型チェックを実行します:
cargo check
E0277が表示されなければOKです。次にテストを実行します:
cargo test
main()のシグネチャを変更した場合は、フルビルドで実行時に問題がないか確認しましょう:
cargo run
クイックリファレンス
- main()内 —
fn main() -> Result<(), Box<dyn Error>>に変更し、末尾にOk(())を追加する - 通常の関数内 — 戻り値の型を
Result<T, E>またはOption<T>に変更し、値をOk()またはSome()でラップして返す - クロージャ内 — クロージャが
Resultを返すようにし、Result<Vec<_>, _>にcollectする - 型の混在 —
.ok_or()でOption → Resultに変換、または.ok()でResult → Optionに変換する
E0277はRustの中でも親切なエラーの一つです。どの関数の戻り値の型を変更すべきか、問題のある?がどこにあるかを正確に教えてくれます。戻り値の型が揃えば、?はシステムプログラミング言語の中でも最もシンプルなエラー伝播パターンの一つになります。

