rust: 文法エラーの時に .unwrap() や ? を適当に試さないようになりたい
2025/10/18
Rust を vscode で書いていると、文法エラーになるときは事前に指摘してくれることが多い。
なんとなくな場所も指摘してくれるので、引数がどうのこうのだと定義を見直して確認している。
が、これが戻り値関連だと、まずは .unwrap()
を付けてみたり ?
を付けてみたりして「これでなんとかなれ!」って気持ちで適当に試してしまう。
これはよろしくなかろう。
.unwrap()
が使えるところ
だいたいそういうときは、戻り値が単独のつもりだったけど Result<T, E>
だったということが多い。
.unwrap()
や ?
を付けてしまうのはその体験からだろう。
?
は上側に判断を委ねるからまだよいが、.unwrap()
は Result<T, E>
の T
を返すか、さもなくば死(panic)を、という感じだ。
なので、.unwrap()
はなるべく上側の方がよいし、.unwrap()
よりは ?
の方がよかろう。
その代わり、戻り値もまた Result<T, E>
になるので変更する影響は大きくなりがちだ。
どうしても戻り値を変更したくないなら .expect()
で期待する動作を示すようにするか。
よっぽど予想外な場合はエラー処理をわざわざ作らず .unwrap()
で終わらせた方が「なんか異常だ」と思ってもらえるかもしれない。
Linux で動かす前提なのにそうじゃなかったとか、Arm64 専用ツールなのにそうじゃなかったとか。
私がよく調べてないのは .unwrap()
がどういうシーンなら使えるのかだ。
doc.rust-lang.org で検索すると、単語一致で “unwrap” を持っているのは Option::unwrap
と Result::unwrap
だけだった。
Rust の enum
って、他の言語にもある列挙型の enum
と C/C++ の union
が混ざったような型だなあと思ってる。
union
的な要素は「持つ値は 1つだけ」というだけで C/C++ のようなキャストっぽい機能は無いがね。
Result<T, E>
Result<T, E>.unwrap() は T
か E
しか返さないのでわかりやすい。
T
は Ok(T)
に、E
は Err(E)
になる。
Ok()
や Err()
に何か実装があるわけではなく、そういう意味づけがある enum値というだけのようだ。
.unwrap()
には where
があるので E
が何でもよいわけでは無くデバッグ出力っぽいことができないとダメそうだ。
そういうときでも .unwrap_なんちゃら()
を探せばほどよいのがあるだろう。
Return<>
を使いたいだけであればそういう制約はない。
pub enum Result<T, E> {
/// Contains the success value
#[lang = "Ok"]
#[stable(feature = "rust1", since = "1.0.0")]
Ok(#[stable(feature = "rust1", since = "1.0.0")] T),
/// Contains the error value
#[lang = "Err"]
#[stable(feature = "rust1", since = "1.0.0")]
Err(#[stable(feature = "rust1", since = "1.0.0")] E),
}
......
pub fn unwrap(self) -> T
where
E: fmt::Debug,
{
match self {
Ok(t) => t,
Err(e) => unwrap_failed("called `Result::unwrap()` on an `Err` value", &e),
}
}
......
Option<T>
[Option
Rust には言語として null
的な定義は無い。その代わりになる enum Option
を標準ライブラリに用意した。
言語としてではなくライブラリとして最初からあるというだけなので、自分で同じようなことをやってもよいということだ。
Rust として null
を組み入れたくはないけど便利だよねということらしい。
.unwrap()
は None
側で panic するので Result
のような制約はない。
その分、なんで panic したのかはわかりづらいかもしれん。
pub enum Option<T> {
/// No value.
#[lang = "None"]
#[stable(feature = "rust1", since = "1.0.0")]
None,
/// Some value of type `T`.
#[lang = "Some"]
#[stable(feature = "rust1", since = "1.0.0")]
Some(#[stable(feature = "rust1", since = "1.0.0")] T),
}
......
pub const fn unwrap(self) -> T {
match self {
Some(val) => val,
None => unwrap_failed(),
}
}
......
?
が使えるところ
演算子の表の最後に ?
があり「エラー委譲」とある。英文では “Error propagation”。
propagation は委譲というよりは伝播っていう感じがする。
?
は、Ok(T)
なら T
が戻されて続く。Err(E)
なら E
が戻されるが return E
になる。
正常時は .unwrap()
と同じで、エラーの場合は呼び元にエラーが戻される。
しかし、標準ライブラリにあるからといって ?
が Err()
専用になっているとは思えない。
オーバーロードするしくみがある。
“Experimental” となっているが ?
になりそうなのがこれしかない。
branch()
は ?
が使われたときに値を返す(Continue
)か呼び出し元に伝播させるか(Break
)を判定する処理、from_output()
が ?
で正常系だったときに返す値だそうだ。
Result
にあった Try
はこれ。
impl<T, E> const ops::Try for Result<T, E> {
type Output = T;
type Residual = Result<convert::Infallible, E>;
#[inline]
fn from_output(output: Self::Output) -> Self {
Ok(output)
}
#[inline]
fn branch(self) -> ControlFlow<Self::Residual, Self::Output> {
match self {
Ok(v) => ControlFlow::Continue(v),
Err(e) => ControlFlow::Break(Err(e)),
}
}
}
Option
にあった Try
はこれ。
impl<T> const ops::Try for Option<T> {
type Output = T;
type Residual = Option<convert::Infallible>;
#[inline]
fn from_output(output: Self::Output) -> Self {
Some(output)
}
#[inline]
fn branch(self) -> ControlFlow<Self::Residual, Self::Output> {
match self {
Some(v) => ControlFlow::Continue(v),
None => ControlFlow::Break(None),
}
}
}
これ以上は深追いすまい。
ともかく、?
が特定のライブラリに依存していないことは分かった。
慣れてくると、下の方のメソッドでは ?
で返す方が楽になってきた。
そうしなかったら、自分で match
を使ってエラーメッセージを書いてエラーログを吐くみたいなことになるだろう。
おまけ1: Option を anyhow::Result にしたい
全体的に Result<T, E>
でエラーを伝播させていたのだが、Option<T>
を返すメソッドもある。
そういう場合はどちらの戻り値(ここでは Result
)に一本化したいだろう。
今やっている例を挙げると、下側からどういう E
が来るのかわからないので anyhow::Result
を使っている。
Result<()>
を返すメソッドの中で Option<u32>
を返すメソッドがあるので何とかしたい。
?
を使ったらうまいことやってくれないかと期待したが、さすがにダメそうだった。
Gemini か Copilot にやってもらうと match
のアームで None => { return Err(anyhow::anyhow!("エラーが起きた")); },
のようにしていた。
文字列型のエラーを返したいだけにしては大げさ気がするが、anyhow
だからこうなるんだろうか。
ネットで検索すると Option
を Result
にする例として .ok_or(値)
や .ok_or_else(関数)
などを使うそうだ。
こんな感じで変換できそうだ。
.ok_or()
で .to_owned()
を使っているのは extension で修正候補が出たから使っただけだ。
to_owned() は借用データから所有データを作り出すものだそうだ。
なら .clone()
でいいんじゃないのと思ったがダメだったが .to_string()
は通る。
use std::process;
fn ldl_hdl(msg: &str) -> Option<String> {
Some(format!("Your {} is too high.", msg))
// None
}
fn cholesterol(msg: &str) -> Result<String, String> {
ldl_hdl(msg).ok_or("None!!!".to_owned())
}
fn main() {
let ret = match cholesterol("LDL") {
Ok(s) => s,
Err(e) => {
eprintln!("cholesterol() error: {}", e.to_string());
process::exit(1)
},
};
println!("{}", ret);
}
固定文字列なんだから借りるも何もないやんと思ってしまうが、その考え方を止めないといつまで経っても理解が進まんな。
おまけ2: golang でいう %w 的な何かはできるかわからん
Go言語で err
を伝播させるとき fmt.Errorf("%w", err)
を使うと経由したエラーが errors.Is()
などで判定できる。
package main
import (
"errors"
"fmt"
)
var errAAA = fmt.Errorf("aaa error occurred")
var errBBB = fmt.Errorf("bbb error occurred")
var errCCC = fmt.Errorf("ccc error occurred")
func main() {
err := aaa()
if err != nil {
fmt.Println("Error:", err)
if errors.Is(err, errCCC) {
fmt.Println("Caught errCCC")
}
if errors.Is(err, errBBB) {
fmt.Println("Caught errBBB")
}
if errors.Is(err, errAAA) {
fmt.Println("Caught errAAA")
}
}
}
func aaa() error {
var err = bbb()
return fmt.Errorf("aaa error occurred: %w", err)
}
func bbb() error {
var err = ccc()
return fmt.Errorf("bbb error occurred: %w, %w", err, errBBB)
}
func ccc() error {
var err = errCCC
return fmt.Errorf("ccc error occurred: %w", err)
}
これを実行すると %w
を通した errCCC
と errBBB
を分かってくれる。
ログ出力にするのが目的なら文字列を書かないといけないのでそんなにありがたみはないが、埋め込んでおいて損はないと思う。
$ go run main.go
Error: aaa error occurred: bbb error occurred: ccc error occurred: ccc error occurred, bbb error occurred
Caught errCCC
Caught errBBB
同じような機能が Rust にもあると思うのだ。
だいたい .unwrap()
が引き剥がす方だから、何もしなければ wrap しているということだ。
ならばそういうキーワードで検索してみよう。
検索よりも AI に尋ねた方が速い気はするが、たまにはよかろう。
- Multiple error types
- 18.5.1:
Result<Option<ほげほげ>, ふがふがError>
- 18.5.2: エラーを自作
- 18.5.3:
Box
型にする - 18.5.4:
Box
型と?
- 18.5.5:
Box
を使わない方法
- 18.5.1:
まだ Box
型を勉強してないので、まだいいかな。
なお ChatGPT氏 としては thiserror
(library) + anyhow
(app) らしい。