// book chapter
動かして学ぶRustプログラミング問題集第12章 OptionとResult.
第11章で、列挙型とmatch式を身につけました。この章では、その2つが実際にどう使われているのか——標準ライブラリのOption型とResult型——を13問で扱います。
プログラムを書いていると、必ず2つの場面に出会います。「探したものが見つからなかった」と「処理に失敗した」です。多くの言語ではnullを返したり例外を投げたりして表しますが、どちらもチェックを忘れたコードがそのまま動いてしまい、実行時に初めて問題が表面化します。
Rustにはnullも例外もありません。代わりに、値がないかもしれないことをOption型、失敗するかもしれないことをResult型というただの列挙型で表します。列挙型なので、扱うにはmatchが必要です。そしてmatchには網羅性チェックがあるので、「値がなかった場合」「失敗した場合」を書き忘れたコードはコンパイルを通りません。第11章で学んだ仕組みが、そのまま安全性の土台になっています。
進め方は第11章までと同じです。各問題の冒頭に関連する辞書へのリンクを挙げているので、まずはリンク先で必要な知識を確認してから取り組んでください。
01 - Option型
Option型とmatch式に関する問題です。
show_stock関数の中身を書いて、在庫が分かる場合と分からない場合を出し分けてください。
在庫は3個です
在庫が分かりませんfn show_stock(stock: Option<u32>) {
// 次のように出力せよ
// 在庫数が設定されている → 在庫は〇〇個です
// 在庫数が設定されていない → 在庫が分かりません
}
fn main() {
show_stock(Some(3));
show_stock(None);
}Playgroundで開く解答例と解説
fn show_stock(stock: Option<u32>) {
// 次のように出力せよ
// 在庫数が設定されている → 在庫は〇〇個です
// 在庫数が設定されていない → 在庫が分かりません
match stock {
Some(count) => println!("在庫は{count}個です"),
None => println!("在庫が分かりません"),
}
}
fn main() {
show_stock(Some(3));
show_stock(None);
}Playgroundで開くOption型は、値が「ある」か「ない」かを表す標準ライブラリの列挙型です。定義は驚くほど単純で、次の2バリアントしかありません。
enum Option<T> {
Some(T), // 値がある
None, // 値がない
}Some(T)は第11章で学んだデータを持つバリアントそのもので、Noneはデータを持たないバリアントです。したがって扱い方も第11章のままで、matchでバリアントごとに分岐し、Some(count)のパターンで中の値を取り出します。
Tは「どんな型でも入る」という意味の書き方(ジェネリクス)です。今回はOption<u32>なのでSomeの中身はu32、Option<String>ならStringになります。
SomeとNoneはOption::Some・Option::Noneと書かずにそのまま使えます。よく使うため、標準ライブラリが最初から名前を使えるようにしてくれているためです。
Some(3)のように書けばOption<u32>の値が作れます。show_stock(None)のほうは、引数の型がOption<u32>と決まっているので、Noneだけ書けばどのOptionなのかがコンパイラに伝わります。
02 - Option<T>とTは別の型
Option型とmatch式に関する問題です。
次のコードはコンパイルエラー(E0369)になります。matchで値を取り出す形に書き換えて修正してください。在庫が分からない場合は0個として扱います。
合計: 5個fn main() {
let stock: Option<u32> = Some(3);
let total = stock + 2;
println!("合計: {total}個");
}Playgroundで開く解答例と解説
fn main() {
let stock: Option<u32> = Some(3);
let total = stock + 2;
let total = match stock {
Some(count) => count + 2,
None => 0 + 2, // 在庫が分からない場合は0個として扱う
};
println!("合計: {total}個");
}Playgroundで開くエラーメッセージはcannot add '{integer}' to 'Option<u32>'(E0369)です。「Option<u32>に整数は足せない」と言われています。
Option<u32>とu32は別の型です。Some(3)は「3という値が入った箱」であって、3そのものではありません。箱のまま計算することはできないので、中身を取り出す手続きが必要になります。
そして取り出す唯一の入口がmatch(およびこの後の問題で扱うif let・let-else)です。パターンで取り出そうとすれば、必然的に「Noneだったらどうするか」も書かされます。
let total = match stock {
Some(count) => count + 2,
// Noneのアームがないとエラー: E0004
};これが「nullチェック漏れが起きない」という言葉の中身です。チェックを忘れる余地がそもそもありません。第11章で学んだ網羅性チェックが、そのまま安全装置として働いています。
「在庫が分からない場合は0個として扱う」といった判断は必ず書き手がすることになる、という点も大事です。0とみなすのか、エラーにするのか、処理を打ち切るのか。言語が勝手に決めるのではなく、コードとして残ります。
03 - Optionを返す関数
Option型と関数に関する問題です。
メニューから価格を探す関数find_priceを実装して、テストに合格させてください。
// メニューから価格を探す関数find_priceを定義せよ
// 引数 : 商品名(&str)
// 戻り値: 見つかればSome(価格)、見つからなければNone
// メニュー: コーヒー 500円 / 紅茶 450円 / ジュース 400円
#[test]
fn test_found() {
assert_eq!(find_price("コーヒー"), Some(500));
assert_eq!(find_price("紅茶"), Some(450));
assert_eq!(find_price("ジュース"), Some(400));
}
#[test]
fn test_not_found() {
assert_eq!(find_price("ビール"), None);
}Playgroundで開く解答例と解説
// メニューから価格を探す関数find_priceを定義せよ
// 引数 : 商品名(&str)
// 戻り値: 見つかればSome(価格)、見つからなければNone
// メニュー: コーヒー 500円 / 紅茶 450円 / ジュース 400円
fn find_price(name: &str) -> Option<u32> {
match name {
"コーヒー" => Some(500),
"紅茶" => Some(450),
"ジュース" => Some(400),
_ => None,
}
}
#[test]
fn test_found() {
assert_eq!(find_price("コーヒー"), Some(500));
assert_eq!(find_price("紅茶"), Some(450));
assert_eq!(find_price("ジュース"), Some(400));
}
#[test]
fn test_not_found() {
assert_eq!(find_price("ビール"), None);
}Playgroundで開く「探す」処理の戻り値はOptionにする、というのがRustの定番です。見つかったときはSome(値)、見つからなかったときはNoneを返します。
戻り値の型にOption<u32>と書いてあるだけで、この関数を使う側は「見つからないことがある」と気付けます。ドキュメントを読まなくても、型が仕様を語っています。
matchの対象が列挙型ではなく&strになっている点にも注目してください。第11章の問題07で数値のパターンを書いたのと同じで、文字列リテラルもパターンとして書けます。&strが取りうる値は無数にあるので、最後に包括パターンの_が必要です。
_ => Noneという1行が、この関数の「見つからなかった場合」を表しています。第11章で「列挙型では_を避ける」と書きましたが、こちらは値を列挙しきれない型なので_が適切な場面です。
04 - if let式
if let式とOption型に関する問題です。
show_stock関数の中身をif let式で書いて、在庫が分かるときだけ出力してください。分からないときは何も出力しません。
在庫は3個です
在庫は10個ですfn show_stock(stock: Option<u32>) {
// if let式を使い、Someのときだけ「在庫は〇〇個です」と出力せよ
// Noneのときは何も出力しないこと
}
fn main() {
show_stock(Some(3));
show_stock(None);
show_stock(Some(10));
}Playgroundで開く解答例と解説
fn show_stock(stock: Option<u32>) {
// if let式を使い、Someのときだけ「在庫は〇〇個です」と出力せよ
// Noneのときは何も出力しないこと
if let Some(count) = stock {
println!("在庫は{count}個です");
}
}
fn main() {
show_stock(Some(3));
show_stock(None);
show_stock(Some(10));
}Playgroundで開くmatchで書くと、何もしないNoneのアームが必要になります。
match stock {
Some(count) => println!("在庫は{count}個です"),
None => {} // 何もしないのに書かなければならない
}この「1つのパターンだけ処理したい」という場面のための構文がif let式です。
if let Some(count) = stock {
println!("在庫は{count}個です");
}読み方に少しコツがあります。if let パターン = 対象の値という順番で、左がパターン、右が調べたい値です。=は代入ではなく「このパターンに当てはまるか」の照合を表します。当てはまればブロックが実行され、パターンの中の変数(count)がブロックの中で使えます。
matchとの違いは1点だけ、網羅性チェックが働かないことです。書かなかったパターンは黙って無視されます。
match |
if let |
|
|---|---|---|
| 扱えるパターン | すべて網羅する必要がある | 1つだけでよい |
| 網羅性チェック | あり | なし |
| 向いている場面 | 分岐ごとに処理が違う | 1つの場合だけ処理したい |
網羅性チェックはmatchの大きな価値なので、if letは「残りは本当に何もしなくてよい」と言い切れるときに使ってください。
05 - if letとelse
if let式と包括パターンに関する問題です。
show_coupon関数の中身をif let式とelseで書いてください。
300円引きのクーポンがあります
クーポンはありませんfn show_coupon(coupon: Option<u32>) {
// if let ... elseで次のように出力せよ
// Some(amount) → 〇〇円引きのクーポンがあります
// None → クーポンはありません
}
fn main() {
show_coupon(Some(300));
show_coupon(None);
}Playgroundで開く解答例と解説
fn show_coupon(coupon: Option<u32>) {
// if let ... elseで次のように出力せよ
// Some(amount) → 〇〇円引きのクーポンがあります
// None → クーポンはありません
if let Some(amount) = coupon {
println!("{amount}円引きのクーポンがあります");
} else {
println!("クーポンはありません");
}
}
fn main() {
show_coupon(Some(300));
show_coupon(None);
}Playgroundで開くif letのelseは「パターンに当てはまらなかった残り全部」を引き受けます。役割としてはmatchの_(包括パターン)と同じで、書き方が違うだけです。
// if let
if let Some(amount) = coupon {
println!("{amount}円引きのクーポンがあります");
} else {
println!("クーポンはありません");
}
// match
match coupon {
Some(amount) => println!("{amount}円引きのクーポンがあります"),
_ => println!("クーポンはありません"),
}elseのブロックでは、パターンで束縛した変数(amount)は使えません。当てはまらなかった以上、中の値は存在しないためです。
今回のOptionのようにバリアントが2つしかない場合、if let ... elseとmatchのどちらでも書けます。選び方の目安はこうです。
- 2つの分岐が対等な意味を持つなら
match。SomeとNoneを並べて書いたほうが、両方を扱っていることが目に見えます - 片方が本筋で、もう片方は例外的な処理なら
if let ... else
バリアントが3つ以上ある列挙型では、if letだと残りがelseにまとめられてしまうので、基本的にmatchを選びます。
06 - let-else文
let-else文とreturn式に関する問題です。
次のコードはコンパイルエラー(E0425)になります。let-else文でstockからcountを取り出して修正してください。
在庫3個、合計1500円です
在庫が分かりませんfn print_total(stock: Option<u32>) {
// let-else文でstockからcountを取り出せ
// Noneの場合は「在庫が分かりません」と出力して処理を打ち切ること
println!("在庫{count}個、合計{}円です", count * 500);
}
fn main() {
print_total(Some(3));
print_total(None);
}Playgroundで開く解答例と解説
fn print_total(stock: Option<u32>) {
// let-else文でstockからcountを取り出せ
// Noneの場合は「在庫が分かりません」と出力して処理を打ち切ること
let Some(count) = stock else {
println!("在庫が分かりません");
return;
};
println!("在庫{count}個、合計{}円です", count * 500);
}
fn main() {
print_total(Some(3));
print_total(None);
}Playgroundで開くエラーメッセージはcannot find value 'count' in this scope(E0425)です。countを作るところがまだ書かれていません。
let-else文は「取り出せたら続ける、取り出せなければ打ち切る」を書くためのlet文です。
let Some(count) = stock else {
println!("在庫が分かりません");
return;
};if letとの一番の違いは、取り出した値がブロックの外で使えることです。if letは{}の中でしかcountを使えませんが、let-elseは以降の行でずっと使えます。
// if let: countはブロックの中だけ
if let Some(count) = stock {
println!("{count}個");
}
// ここではcountは使えない
// let-else: 以降ずっと使える
let Some(count) = stock else { return };
println!("{count}個"); // 使えるこれは処理の形に効いてきます。if letで書くと本筋の処理がブロックの中に入り、条件が増えるほど右へ右へとネストが深くなります。let-elseなら「ダメな場合を先に片付けて抜ける」と書けるので、本筋がネストせずまっすぐ並びます。
elseブロックには必ず処理を打ち切る何かを書かなければなりません。returnのほか、ループの中ならbreakやcontinueが使えます。
let Some(count) = stock else {
println!("在庫が分かりません"); // 打ち切らずに抜けてしまう
};理由を考えると当然で、elseを素通りしたらcountに入れる値がありません。「打ち切ること」が構文として強制されています。
07 - Result型
Result型とmatch式に関する問題です。
show関数の中身を書いて、成功時と失敗時を出し分けてください。
1人あたり750円です
エラー: 人数が0人ですfn show(result: Result<u32, String>) {
// match式で次のように出力せよ
// 成功時 → 1人あたり〇〇円です
// 失敗時 → エラー: 〇〇
}
fn main() {
show(Ok(750));
show(Err(String::from("人数が0人です")));
}Playgroundで開く解答例と解説
fn show(result: Result<u32, String>) {
// match式で次のように出力せよ
// 成功時 → 1人あたり〇〇円です
// 失敗時 → エラー: 〇〇
match result {
Ok(amount) => println!("1人あたり{amount}円です"),
Err(reason) => println!("エラー: {reason}"),
}
}
fn main() {
show(Ok(750));
show(Err(String::from("人数が0人です")));
}Playgroundで開くResult型も、Optionと同じく標準ライブラリの列挙型です。
enum Result<T, E> {
Ok(T), // 成功。結果の値を持つ
Err(E), // 失敗。エラーの値を持つ
}Optionとの違いは、失敗した側も値を持てることです。Noneは「ない」としか言えませんが、Errは「なぜ失敗したのか」を運べます。
Option<T> |
Result<T, E> |
|
|---|---|---|
| 成功 | Some(T) |
Ok(T) |
| 失敗 | None(理由なし) |
Err(E)(理由あり) |
| 使う場面 | 値の有無だけが問題 | 失敗の理由を伝えたい |
Result<u32, String>は「成功すればu32、失敗すればStringのエラーメッセージ」という意味です。エラーの型はStringでなくてもよく、第11章で学んだ自作の列挙型を使うこともよくあります(問題10で扱います)。
扱い方はOptionとまったく同じで、matchでバリアントごとに分岐します。OkとErrも、Some・Noneと同様にResult::を付けずそのまま使えます。
なぜ例外ではなくこの形なのでしょうか。例外は関数のシグネチャに現れないため、どの呼び出しが失敗しうるのかがコードから読み取れません。Resultなら戻り値の型を見るだけで失敗の可能性が分かり、しかもmatchの網羅性チェックによってErrの処理を書き忘れられません。
08 - Resultを返す関数
Result型と関数に関する問題です。
在庫から商品を出荷する関数shipを実装して、テストに合格させてください。
// 在庫stockからcount個を出荷する関数shipを定義せよ
// 在庫が足りていれば、出荷後の残り在庫数をOkで返す
// 足りなければ「在庫が足りません(在庫〇〇個)」というErrを返す
#[test]
fn test_ship_ok() {
assert_eq!(ship(10, 3), Ok(7));
assert_eq!(ship(5, 5), Ok(0));
}
#[test]
fn test_ship_error() {
assert_eq!(ship(2, 5), Err(String::from("在庫が足りません(在庫2個)")));
assert_eq!(ship(0, 1), Err(String::from("在庫が足りません(在庫0個)")));
}Playgroundで開く解答例と解説
// 在庫stockからcount個を出荷する関数shipを定義せよ
// 在庫が足りていれば、出荷後の残り在庫数をOkで返す
// 足りなければ「在庫が足りません(在庫〇〇個)」というErrを返す
fn ship(stock: u32, count: u32) -> Result<u32, String> {
if count > stock {
return Err(format!("在庫が足りません(在庫{stock}個)"));
}
Ok(stock - count)
}
#[test]
fn test_ship_ok() {
assert_eq!(ship(10, 3), Ok(7));
assert_eq!(ship(5, 5), Ok(0));
}
#[test]
fn test_ship_error() {
assert_eq!(ship(2, 5), Err(String::from("在庫が足りません(在庫2個)")));
assert_eq!(ship(0, 1), Err(String::from("在庫が足りません(在庫0個)")));
}Playgroundで開く失敗しうる処理を書くときの基本形です。押さえておきたい点が3つあります。
失敗のチェックを先に済ませる
if count > stockで足りない場合を先に判定し、return Err(...)で早期リターンしています。第4章で学んだreturn式の出番です。こうしておけば、それ以降の行は「在庫は足りている」と分かった状態で書けます。第10章のwithdrawと同じ組み立て方です。
成功の値はOkで包む
stock - countをそのまま返すことはできません。戻り値の型がResult<u32, String>なので、Ok(stock - count)と包む必要があります。OptionのときのSomeと同じで、箱に入れて返すイメージです。
エラーメッセージはformat!で組み立てる
第6章で学んだformat!で、在庫数を埋め込んだStringを作っています。println!が画面に出力するのに対し、format!は文字列を作って返すマクロでした。失敗の理由を呼び出し元へ「値として」渡せるのがResultの強みなので、必要な情報を入れておきます。
u32の引き算にも注意が必要でした。count > stockのチェックを飛ばしてstock - countを計算すると、負の値になるところで実行時にプログラムが停止します。チェックが先、計算が後です。
09 - ?演算子でエラーを伝播する
Result型と関数に関する問題です。
ship_twice関数の中のmatchの入れ子を、?演算子を使って書き換えてください。出力は変わりません。
残り5個
エラー: 在庫が足りません(在庫2個)fn ship(stock: u32, count: u32) -> Result<u32, String> {
if count > stock {
return Err(format!("在庫が足りません(在庫{stock}個)"));
}
Ok(stock - count)
}
fn ship_twice(stock: u32, first: u32, second: u32) -> Result<u32, String> {
// 次のmatchの入れ子を?演算子で書き換えよ(動作は変えないこと)
match ship(stock, first) {
Ok(rest) => match ship(rest, second) {
Ok(last) => Ok(last),
Err(reason) => Err(reason),
},
Err(reason) => Err(reason),
}
}
fn main() {
match ship_twice(10, 3, 2) {
Ok(rest) => println!("残り{rest}個"),
Err(reason) => println!("エラー: {reason}"),
}
match ship_twice(10, 8, 5) {
Ok(rest) => println!("残り{rest}個"),
Err(reason) => println!("エラー: {reason}"),
}
}Playgroundで開く解答例と解説
fn ship(stock: u32, count: u32) -> Result<u32, String> {
if count > stock {
return Err(format!("在庫が足りません(在庫{stock}個)"));
}
Ok(stock - count)
}
fn ship_twice(stock: u32, first: u32, second: u32) -> Result<u32, String> {
// 次のmatchの入れ子を?演算子で書き換えよ(動作は変えないこと)
match ship(stock, first) {
Ok(rest) => match ship(rest, second) {
Ok(last) => Ok(last),
Err(reason) => Err(reason),
},
Err(reason) => Err(reason),
}
let rest = ship(stock, first)?;
let last = ship(rest, second)?;
Ok(last)
}
fn main() {
match ship_twice(10, 3, 2) {
Ok(rest) => println!("残り{rest}個"),
Err(reason) => println!("エラー: {reason}"),
}
match ship_twice(10, 8, 5) {
Ok(rest) => println!("残り{rest}個"),
Err(reason) => println!("エラー: {reason}"),
}
}Playgroundで開く12行あったmatchの入れ子が3行になりました。
?はResultを返す式の後ろに付ける演算子で、次のmatchとまったく同じ意味です。
// この1行は
let rest = ship(stock, first)?;
// おおよそこう書いたのと同じ
let rest = match ship(stock, first) {
Ok(value) => value, // 成功なら中身を取り出して続行
Err(reason) => return Err(reason), // 失敗ならその場で関数から抜ける
};成功なら中身を取り出し、失敗なら即座に呼び出し元へ返す。これだけです。失敗を上へ渡していくことをエラーの伝播と呼び、?はそのための専用の記法です。
?が書ける場所には条件があります。その関数の戻り値もResultでなければなりません。Errをそのままreturnするので、返せる型でなければ成立しないためです。Optionを返す関数の中でも?は使え、その場合はNoneが返ります。
?を使うと、コードの見た目が「成功したときの流れ」だけになります。ship_twiceを読むと「出荷して、また出荷して、結果を返す」と上から素直に読め、失敗時の分岐は?の1文字に畳まれています。失敗しうる処理を何段も重ねるコードでは、この差が大きく効いてきます。
なお、let last = ship(rest, second)?; Ok(last)はship(rest, second)とだけ書いても同じ結果になります。?で開けた箱をOkで包み直しているだけだからです。今回は?の動きを見せるためにあえて2行に分けています。
10 - unwrap_orで既定値を使う
unwrapとOption型に関する問題です。
mainの中の2つのmatchを、unwrap_orを使って1行ずつに書き換えてください。出力は変わりません。
コーヒーの在庫は3個です
紅茶の在庫は0個ですfn main() {
let coffee: Option<u32> = Some(3);
let tea: Option<u32> = None;
// 次の2つのmatchをunwrap_orで書き換えよ(動作は変えないこと)
let coffee_stock = match coffee {
Some(count) => count,
None => 0,
};
let tea_stock = match tea {
Some(count) => count,
None => 0,
};
println!("コーヒーの在庫は{coffee_stock}個です");
println!("紅茶の在庫は{tea_stock}個です");
}Playgroundで開く解答例と解説
fn main() {
let coffee: Option<u32> = Some(3);
let tea: Option<u32> = None;
// 次の2つのmatchをunwrap_orで書き換えよ(動作は変えないこと)
let coffee_stock = match coffee {
Some(count) => count,
None => 0,
};
let coffee_stock = coffee.unwrap_or(0);
let tea_stock = match tea {
Some(count) => count,
None => 0,
};
let tea_stock = tea.unwrap_or(0);
println!("コーヒーの在庫は{coffee_stock}個です");
println!("紅茶の在庫は{tea_stock}個です");
}Playgroundで開く8行が2行になりました。問題02のtipで触れたunwrap系のメソッドです。
unwrap_or(既定値)は、Someなら中身を返し、Noneなら引数の既定値を返します。書き換え前のmatchとやっていることは同じで、Noneのアームに書いていた値が引数に移っただけです。
// match
let coffee_stock = match coffee {
Some(count) => count,
None => 0,
};
// unwrap_or
let coffee_stock = coffee.unwrap_or(0);同じメソッドがResultにもあります。Errのときに既定値を返す、という点だけが違います。値を取り出すメソッドは他にもいくつかあり、unwrapに一覧があります。
便利ですが、使ってよいのは既定値が事実として正しい場面だけです。今回は在庫が分からないものを0個として扱うので筋が通っています。一方、価格が分からない商品をunwrap_or(0)で0円として扱えば、存在しない価格を勝手に作り出すことになります。この線引きは問題12で扱います。
11 - unwrapとexpect
unwrapとResult型に関する問題です。
次のコードは実行するとパニックして止まります。止まった理由が伝わるように、2つのunwrapをexpectへ書き換えてください。
1回目の出荷後: 残り7個
thread 'main' panicked at src/main.rs:14:30:
在庫は足りているはず: "在庫が足りません(在庫7個)"
note: run with `RUST_BACKTRACE=1` environment variable to display a backtraceパニックのメッセージは、thread 'main'の後ろにスレッドの番号が入ることがあり、src/main.rs:14:30の行と桁も書いたコードによって変わります。在庫は足りているはず:から始まる行が出ていれば正解です。
fn ship(stock: u32, count: u32) -> Result<u32, String> {
if count > stock {
return Err(format!("在庫が足りません(在庫{stock}個)"));
}
Ok(stock - count)
}
fn main() {
// 2つのunwrapを、失敗した理由が伝わるexpectへ書き換えよ
let rest = ship(10, 3).unwrap();
println!("1回目の出荷後: 残り{rest}個");
let rest = ship(rest, 9).unwrap();
println!("2回目の出荷後: 残り{rest}個");
}Playgroundで開く解答例と解説
fn ship(stock: u32, count: u32) -> Result<u32, String> {
if count > stock {
return Err(format!("在庫が足りません(在庫{stock}個)"));
}
Ok(stock - count)
}
fn main() {
// 2つのunwrapを、失敗した理由が伝わるexpectへ書き換えよ
let rest = ship(10, 3).unwrap();
let rest = ship(10, 3).expect("在庫は足りているはず");
println!("1回目の出荷後: 残り{rest}個");
let rest = ship(rest, 9).unwrap();
let rest = ship(rest, 9).expect("在庫は足りているはず");
println!("2回目の出荷後: 残り{rest}個");
}Playgroundで開くunwrapは、Okなら中身を返し、Errならパニックしてプログラムを止めるメソッドです。Optionに対しても同じで、Someなら中身、Noneならパニックします。
在庫10個から3個、さらに9個を出荷しようとしているので、2回目のshipはErrを返します。matchならErrのアームを書かされるところですが、unwrapは「Errなら止まる」と決め打ちするメソッドなので、コンパイルは通り、実行して初めて止まります。
書き換え前のunwrapが出すメッセージはこうです。
called `Result::unwrap()` on an `Err` value: "在庫が足りません(在庫7個)"止まった場所とErrの中身は分かりますが、書き手がそこで何を期待していたのかは残りません。expectは同じ動作でメッセージを追加できるメソッドで、そこを補えます。
在庫は足りているはず: "在庫が足りません(在庫7個)"The Rust Programming Languageは、成功すると期待した理由を「〜のはず」という形で書くことを勧めています。「在庫は足りているはず」なのに足りなかった、と読めるので、前提のほうが間違っていたことがすぐ分かります。詳しくはunwrapを参照してください。
どちらのメッセージにもErrの中身が続いて表示されます。{:?}と同じ表示(第9章のDebug出力)なので、Stringはダブルクォートで囲まれます。問題08でformat!にわざわざ在庫数を入れておいたことが、ここで効いています。
12 - unwrapを使ってよい場面
unwrap・Option型・if let式に関する問題です。
次のコードは実行するとパニックして止まります。2つのunwrapを、それぞれの場面にふさわしい形へ書き換えてください。
コーヒー: 500円
ビールは取り扱いがありませんfn find_price(name: &str) -> Option<u32> {
match name {
"コーヒー" => Some(500),
"紅茶" => Some(450),
"ジュース" => Some(400),
_ => None,
}
}
fn main() {
// メニューにある商品なので、必ず見つかる
let coffee = find_price("コーヒー").unwrap();
println!("コーヒー: {coffee}円");
// 客が注文した商品名。メニューにないこともある
// 見つからない場合は「〇〇は取り扱いがありません」と出力すること
let ordered = "ビール";
let price = find_price(ordered).unwrap();
println!("{ordered}: {price}円");
}Playgroundで開く解答例と解説
fn find_price(name: &str) -> Option<u32> {
match name {
"コーヒー" => Some(500),
"紅茶" => Some(450),
"ジュース" => Some(400),
_ => None,
}
}
fn main() {
// メニューにある商品なので、必ず見つかる
let coffee = find_price("コーヒー").unwrap();
let coffee = find_price("コーヒー").expect("メニューにある商品なので必ず見つかるはず");
println!("コーヒー: {coffee}円");
// 客が注文した商品名。メニューにないこともある
// 見つからない場合は「〇〇は取り扱いがありません」と出力すること
let ordered = "ビール";
let price = find_price(ordered).unwrap();
println!("{ordered}: {price}円");
if let Some(price) = find_price(ordered) {
println!("{ordered}: {price}円");
} else {
println!("{ordered}は取り扱いがありません");
}
}Playgroundで開く2つのunwrapは見た目が同じでも、意味はまったく違います。判断の基準は、取り出せなかったときの振る舞いを決め打ってよい根拠があるかどうかです。
1つ目は、探す名前がコードに直接書かれていて、find_priceの定義を読めば必ずSomeが返ると分かります。ところがコンパイラはfind_priceの戻り値がOption<u32>であることしか見ないので、Noneの可能性を消してくれません。書き手のほうが多くを知っている場面なので、取り出してしまってかまいません。ただしunwrapではなくexpectにして、なぜ見つかるはずなのかを残しておきます。
2つ目は、注文された商品名次第でNoneになります。ここでunwrapを使うと、メニューにない商品を注文されただけでプログラムが止まってしまいます。「取り扱いがありません」と伝えて続ければ済む話なので、if let式で分岐します(matchやlet-elseでも書けます)。
unwrap_or(0)ではいけないのか、とも考えられます。しかし、それではビール: 0円と出力され、存在しない価格を勝手に作り出してしまいます。既定値で埋めてよいのは、問題10の在庫数のように既定値が事実として正しい場面だけです。
取り出し方を、この章で扱った順にまとめておきます。
| 場面 | 取り出し方 |
|---|---|
| 失敗しないと言い切れる根拠がある | expect(根拠をメッセージに書く) |
| 失敗しうるが、既定値で埋めてよい | unwrap_or |
| 失敗しうるので、場合ごとに処理を分けたい | match・if let・let-else |
| 失敗の判断を呼び出し元に任せたい | ?演算子 |
書き分けの感覚は、この章の問題を書き直しながら確かめるのが近道です。
13 - 応用: 在庫管理システム
第7章以降の総復習として、構造体・列挙型・Option型・Result型・メソッドをすべて組み合わせた問題です。
商品の在庫を管理するInventoryを実装して、テストに合格させてください。
まず、扱う型を2つ定義します。
| 型 | 種類 | 内容 |
|---|---|---|
Item |
構造体 | name(String)・price(u32)・stock(u32) |
StockError |
列挙型 | NotFound(商品なし)・OutOfStock { stock: u32 }(在庫不足。現在庫を持つ) |
そのうえで、items(Vec<Item>)を持つ構造体Inventoryに次を定義します。
| 名前 | 種類 | 内容 |
|---|---|---|
new() |
関連関数 | 空の在庫を作って返す |
add(name, price, stock) |
メソッド | 商品を1つ追加する |
find_price(name) |
メソッド | 価格をOption<u32>で返す。商品がなければNone |
buy(name, count) |
メソッド | 在庫を減らし、合計金額をResult<u32, StockError>で返す |
// 商品を表す構造体Itemを定義せよ(name: String / price: u32 / stock: u32)
// 購入の失敗理由を表す列挙型StockErrorを定義せよ
// NotFound … 商品が見つからない
// OutOfStock { stock: u32 } … 在庫不足(現在の在庫数を持つ)
// テストのassert_eq!で比較するため #[derive(Debug, PartialEq)] を付けること
// 在庫全体を表す構造体Inventoryを定義せよ(items: Vec<Item>)
// Inventoryのimplブロックにnew・add・find_price・buyを定義せよ
fn sample() -> Inventory {
let mut inventory = Inventory::new();
inventory.add("コーヒー豆", 1200, 3);
inventory.add("マグカップ", 800, 10);
inventory
}
#[test]
fn test_find_price() {
let inventory = sample();
assert_eq!(inventory.find_price("コーヒー豆"), Some(1200));
assert_eq!(inventory.find_price("マグカップ"), Some(800));
assert_eq!(inventory.find_price("ビール"), None);
}
#[test]
fn test_buy() {
let mut inventory = sample();
assert_eq!(inventory.buy("コーヒー豆", 2), Ok(2400));
assert_eq!(inventory.buy("コーヒー豆", 1), Ok(1200));
// 在庫を使い切ったので次は失敗する
assert_eq!(
inventory.buy("コーヒー豆", 1),
Err(StockError::OutOfStock { stock: 0 })
);
}
#[test]
fn test_buy_not_found() {
let mut inventory = sample();
assert_eq!(inventory.buy("ビール", 1), Err(StockError::NotFound));
}
#[test]
fn test_buy_out_of_stock() {
let mut inventory = sample();
assert_eq!(
inventory.buy("コーヒー豆", 5),
Err(StockError::OutOfStock { stock: 3 })
);
// 失敗したときは在庫を減らさない
assert_eq!(inventory.buy("コーヒー豆", 3), Ok(3600));
}Playgroundで開く解答例と解説
// 商品を表す構造体Itemを定義せよ(name: String / price: u32 / stock: u32)
struct Item {
name: String,
price: u32,
stock: u32,
}
// 購入の失敗理由を表す列挙型StockErrorを定義せよ
// NotFound … 商品が見つからない
// OutOfStock { stock: u32 } … 在庫不足(現在の在庫数を持つ)
// テストのassert_eq!で比較するため #[derive(Debug, PartialEq)] を付けること
#[derive(Debug, PartialEq)]
enum StockError {
NotFound,
OutOfStock { stock: u32 },
}
// 在庫全体を表す構造体Inventoryを定義せよ(items: Vec<Item>)
struct Inventory {
items: Vec<Item>,
}
// Inventoryのimplブロックにnew・add・find_price・buyを定義せよ
impl Inventory {
fn new() -> Self {
Inventory { items: Vec::new() }
}
fn add(&mut self, name: &str, price: u32, stock: u32) {
self.items.push(Item {
name: String::from(name),
price,
stock,
});
}
fn find_price(&self, name: &str) -> Option<u32> {
for item in &self.items {
if item.name == name {
return Some(item.price);
}
}
None
}
fn buy(&mut self, name: &str, count: u32) -> Result<u32, StockError> {
for item in &mut self.items {
if item.name == name {
if item.stock < count {
return Err(StockError::OutOfStock { stock: item.stock });
}
item.stock -= count;
return Ok(item.price * count);
}
}
Err(StockError::NotFound)
}
}
fn sample() -> Inventory {
let mut inventory = Inventory::new();
inventory.add("コーヒー豆", 1200, 3);
inventory.add("マグカップ", 800, 10);
inventory
}
#[test]
fn test_find_price() {
let inventory = sample();
assert_eq!(inventory.find_price("コーヒー豆"), Some(1200));
assert_eq!(inventory.find_price("マグカップ"), Some(800));
assert_eq!(inventory.find_price("ビール"), None);
}
#[test]
fn test_buy() {
let mut inventory = sample();
assert_eq!(inventory.buy("コーヒー豆", 2), Ok(2400));
assert_eq!(inventory.buy("コーヒー豆", 1), Ok(1200));
// 在庫を使い切ったので次は失敗する
assert_eq!(
inventory.buy("コーヒー豆", 1),
Err(StockError::OutOfStock { stock: 0 })
);
}
#[test]
fn test_buy_not_found() {
let mut inventory = sample();
assert_eq!(inventory.buy("ビール", 1), Err(StockError::NotFound));
}
#[test]
fn test_buy_out_of_stock() {
let mut inventory = sample();
assert_eq!(
inventory.buy("コーヒー豆", 5),
Err(StockError::OutOfStock { stock: 3 })
);
// 失敗したときは在庫を減らさない
assert_eq!(inventory.buy("コーヒー豆", 3), Ok(3600));
}Playgroundで開く第7章以降で学んだものがほぼすべて登場しました。順に見ていきます。
OptionとResultの使い分け
find_priceはOption、buyはResultを返しています。境目は「失敗の理由を伝える必要があるか」です。価格を探して見つからなければNoneで十分ですが、購入の失敗には「そんな商品はない」と「在庫が足りない」の2種類があり、呼び出し側は区別したいはずです。だからResultを選び、エラーの型で理由を表現します。
エラーの型に列挙型を使う
問題08ではResult<u32, String>とエラーをメッセージで表しましたが、ここではStockErrorという列挙型にしました。文字列と違い、呼び出し側がmatchで理由ごとに処理を分けられ、しかも網羅性チェックが効きます。OutOfStock { stock: u32 }のようにエラーが追加の情報を運べるのも列挙型ならではです。実務のRustでは、エラー型を列挙型で定義するのが標準的なやり方です。
#[derive(Debug, PartialEq)]が必要な理由
assert_eq!は2つの値を比較し、違っていたら中身を表示します。そのため比較する型には「等しさを判定する方法」と「表示する方法」が必要です。PartialEqが前者、Debugが後者で、単純な列挙型ならどちらもderiveで自動生成できます。第9章で{:?}のためにDebugを付けたのと同じ仕組みです。
&selfと&mut selfの使い分け
find_priceは探すだけなので&self、addとbuyはitemsを変更するので&mut selfです。第10章で学んだ判断がそのまま出てきます。
for item in &mut self.items
第6章ではfor item in &vecと共有参照で走査しました。書き換えたいときは&mutを付けて排他参照で走査します。こうするとitemは&mut Itemになり、item.stock -= countで中身を更新できます。
失敗するときは何も変えない
buyは在庫が足りないと判定した時点でreturnし、item.stockには一切触れません。「失敗したなら何も起きていない状態にする」のは、状態を持つ処理を書くうえで大事な性質です。test_buy_out_of_stockの最後で、失敗した後も在庫3個が残っていることを確認しています。
見つからなかった場合はループの後に書く
find_priceのNoneとbuyのErr(StockError::NotFound)は、どちらもforループを抜けた後にあります。「最後まで探しても見つからなかった」がそのまま「ループが終わった」という位置に対応していて、returnで途中脱出する形と自然に噛み合います。