Rustの標準ライブラリの便利技:毎日使える小さなテクニック
台所の引き出しには、ピーラーや栓抜き、にんにく絞りのような小さな道具が入っています。大きな包丁1本でも料理はできますが、小さな道具があると、作業が早く、安全になります。Rustの標準ライブラリにも、そんな引き出しがあります。
このページでは、その引き出しの中身を順番に見ていきます。どのテクニックも短く、身近なたとえ、実行できるサンプル、そして「こんなときに使う」の一言をセットにしました。全部を一度に覚える必要はありません。今はざっと目を通しておいて、自分のコードに「なんだかぎこちないな」と感じる書き方が出てきたら、また戻ってきてください。
&mut から値を取り出す:mem::take、replace、swap
Section titled “&mut から値を取り出す:mem::take、replace、swap”ゴミ箱に袋がかかっているところを想像してください。いっぱいになった袋を取り出してもかまいませんが、すぐに新しい袋をかけるのが条件です。ゴミ箱が袋なしの状態になることはありません。
Rustにも同じルールがあります。&mut self しか持っていないときは、フィールドをそのままムーブして取り出すことはできません。構造体に穴が空いてしまうからです。そうしようとすると、コンパイラが E0507 で止めてくれます。std::mem の関数は、必ず何かを代わりに残していくことで、この問題を解決します。
mem::take(&mut x)は値を取り出し、その型のデフォルト値(空のVec、空のString、0、None)を残します。mem::replace(&mut x, new)はnewを入れて、古い値を返してくれます。Option::take()はOptionの中身を取り出し、Noneを残します。
use std::mem;
struct Inbox { unread: Vec<String>, status: String, draft: Option<String>,}
impl Inbox { // 未読メッセージをすべて渡し、空の Vec を残す fn read_all(&mut self) -> Vec<String> { mem::take(&mut self.unread) }
// 新しいステータスを入れ、古いものを受け取る fn set_status(&mut self, new: &str) -> String { mem::replace(&mut self.status, new.to_string()) }
// 下書きがあれば取り出し、None を残す fn send_draft(&mut self) -> Option<String> { self.draft.take() }}
fn main() { let mut inbox = Inbox { unread: vec!["ランチ行く?".to_string(), "請求書 #42".to_string()], status: "オンライン".to_string(), draft: Some("6時に会いましょう".to_string()), };
let messages = inbox.read_all(); println!("読んだ: {:?}、未読のまま: {:?}", messages, inbox.unread);
let old = inbox.set_status("会議中"); println!("ステータスは「{old}」から「{}」に変わりました", inbox.status);
println!("送信: {:?}", inbox.send_draft()); println!("もう一度送信: {:?}", inbox.send_draft());}実行結果:
読んだ: ["ランチ行く?", "請求書 #42"]、未読のまま: []ステータスは「オンライン」から「会議中」に変わりました送信: Some("6時に会いましょう")もう一度送信: Noneclone() はどこにも出てきません。メッセージも古いステータスも、コピーではなくムーブで取り出されています。だから、これらの関数は安全なうえに軽いのです。
仲間があと2つあります。mem::swap(&mut a, &mut b) は、テーブルの上の2枚のお皿を入れ替えるように、2つの値を交換します。Option::replace(new) は Option に新しい値を入れて、古い値を返してくれます。
use std::mem;
fn main() { // 片方のリストを処理している間に、もう片方に注文をためる let mut filling = vec!["注文1", "注文2"]; let mut processing: Vec<&str> = Vec::new();
mem::swap(&mut filling, &mut processing); println!("処理中: {processing:?}、受付中: {filling:?}");
// Option::replace:新しい値を入れて、古い値を受け取る let mut now_playing = Some("曲A"); let previous = now_playing.replace("曲B"); println!("前の曲: {previous:?}、今の曲: {now_playing:?}");}実行結果:
処理中: ["注文1", "注文2"]、受付中: []前の曲: Some("曲A")、今の曲: Some("曲B")こんなときに使う: &mut でしかアクセスできないのに、中の値の所有権が欲しいとき。
スライス:windows、chunks とその仲間
Section titled “スライス:windows、chunks とその仲間”windows(2) は、定規の上をすべらせていく虫眼鏡のようなものです。目盛りを2つずつ見せて、1目盛りずつ進みます。chunks(3) は、卵を3個入りのパックに詰めていくイメージです。最後のパックは満杯にならないこともあります。chunks_exact(3) は満杯のパックだけを渡してくれて、余った卵は .remainder() に取っておきます。
これらのメソッドはどんなスライスにも使えるので、配列やベクタにも使えます。
fn main() { let temps = [12, 15, 14, 19, 21];
// windows(2):となり合う2つずつ。重なりながら進む for pair in temps.windows(2) { println!("{} -> {}: 変化 {}", pair[0], pair[1], pair[1] - pair[0]); }
// chunks(3):3つずつのまとまり。最後は少なくてもよい let emails = ["[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]"]; for batch in emails.chunks(3) { println!("送信先: {batch:?}"); }
// chunks_exact(2):満杯のまとまりだけ。残りは remainder() で待つ let seats = [1, 2, 3, 4, 5]; let pairs = seats.chunks_exact(2); let leftover = pairs.remainder(); for pair in pairs { println!("ペア {pair:?}"); } println!("余り: {leftover:?}");}実行結果:
12 -> 15: 変化 315 -> 14: 変化 -114 -> 19: 変化 519 -> 21: 変化 2送信先: ["[email protected]", "[email protected]", "[email protected]"]送信先: ["[email protected]", "[email protected]"]ペア [1, 2]ペア [3, 4]余り: [5]ほかにも、ループを書かずによくある疑問に答えてくれるスライスのメソッドがあります。split_first() は、先頭の要素と残りの部分を返します。先生が名簿の最初の名前を読み上げて、残りの名簿を手伝いの人に渡すようなものです。split_last() は、同じことを末尾から行います。
fn main() { let command = ["git", "commit", "-m", "誤字を修正"];
// split_first:先頭の要素と、残りのスライス if let Some((program, args)) = command.split_first() { println!("プログラム: {program}、引数: {args:?}"); }
// split_last:最後の要素と、その手前すべて if let Some((message, _rest)) = command.split_last() { println!("メッセージ: {message}"); }
println!("git で始まる? {}", command.starts_with(&["git"])); println!("-m を含む? {}", command.contains(&"-m"));
// iter().rev():スライスを変えずに後ろから順にたどる let history = ["開く", "編集", "保存"]; let undo_order: Vec<_> = history.iter().rev().collect(); println!("元に戻す順番: {undo_order:?}");}実行結果:
プログラム: git、引数: ["commit", "-m", "誤字を修正"]メッセージ: 誤字を修正git で始まる? true-m を含む? true元に戻す順番: ["保存", "編集", "開く"]split_first と split_last が Option を返すのは、空のスライスには先頭の要素がないからです。そのため if let を使っています。
こんなときに使う: となり同士(windows)、まとまりごと(chunks)、「先頭と残り」(split_first)が必要なとき。
いつも使うイテレータの便利メソッド
Section titled “いつも使うイテレータの便利メソッド”map、filter、sum、collect はイテレータのページで紹介しました。ここでは、道具箱のその次の段に入っている道具を見ていきます。
peekable:取る前にのぞく
Section titled “peekable:取る前にのぞく”トランプのカードを配っているところを想像してください。次のカードを引くかどうか決める前に、ちょっとのぞいてみたいことがありますよね。.peekable() を使うと、それができるイテレータになります。.peek() は次の要素を取らずに見せてくれて、.next_if(...) は条件を満たすときだけ取り出します。小さなパーサーにぴったりの機能です。
fn main() { let input = "12+345-6"; let mut chars = input.chars().peekable(); let mut tokens: Vec<String> = Vec::new();
while let Some(c) = chars.next() { if c.is_ascii_digit() { let mut number = c.to_string(); // 次の文字も数字である間だけ取り出す while let Some(d) = chars.next_if(|next| next.is_ascii_digit()) { number.push(d); } tokens.push(number); } else { tokens.push(c.to_string()); } }
println!("{tokens:?}");}実行結果:
["12", "+", "345", "-", "6"]peekable がなかったら、続きの数字を探している途中で + を読んでしまい、その + は失われてしまいます。
こんなときに使う: 次の要素によって、今の要素の扱いが決まるとき。
リストをたどる:zip、enumerate、step_by、take_while、skip_while、chain
Section titled “リストをたどる:zip、enumerate、step_by、take_while、skip_while、chain”| メソッド | イメージ | こんなときに… |
|---|---|---|
zip |
ファスナーが左右の歯を1つずつかみ合わせる | 組になる2つのリストがある |
enumerate |
リストの各行に番号をふる | 要素と一緒にインデックスも欲しい |
step_by(n) |
階段を2段(またはn段)ずつ上る | n個おきに要素が欲しい |
take_while |
苦手なお菓子に当たるまで食べ続ける | 最初の「ダメ」の手前まで、リストの前半が欲しい |
skip_while |
番組が始まるまでCMを飛ばす | 最初の「ダメ」から後ろの残りが欲しい |
chain |
電車の車両を連結する | リストを次々とつなげたい |
fn main() { let names = ["Ana", "Ben", "Chen"]; let scores = [88, 92, 75];
// zip で2つのリストを組にし、enumerate で0から始まる番号をつける for (i, (name, score)) in names.iter().zip(scores.iter()).enumerate() { println!("{}. {name}: {score}", i + 1); }
// step_by:1時間のうち15分ごと let quarters: Vec<u32> = (0..60).step_by(15).collect(); println!("15分ごと: {quarters:?}");
// 配列に into_iter() を使うと、数値そのものが得られる let queue = [3, 5, 8, 12, 4]; let small: Vec<i32> = queue.into_iter().take_while(|n| *n < 10).collect(); let rest: Vec<i32> = queue.into_iter().skip_while(|n| *n < 10).collect(); println!("最初の大きな数の手前: {small:?}、そこから後ろ: {rest:?}");
// chain:リストを1つずつつなげる let morning = ["メール", "朝会"]; let afternoon = ["レビュー", "デプロイ"]; let day: Vec<_> = morning.iter().chain(afternoon.iter()).collect(); println!("{day:?}");}実行結果:
1. Ana: 882. Ben: 923. Chen: 7515分ごと: [0, 15, 30, 45]最初の大きな数の手前: [3, 5, 8]、そこから後ろ: [12, 4]["メール", "朝会", "レビュー", "デプロイ"]take_while は 12 のところで完全に止まることに注目してください。その後ろの 4 は小さい数ですが、調べられることはありません。ここが、すべての要素を調べる filter との違いです。
疑問に答える:fold、position、find_map、partition、flat_map
Section titled “疑問に答える:fold、position、find_map、partition、flat_map”| メソッド | イメージ | こんなときに… |
|---|---|---|
sum |
電卓の「+」キー | ただ合計が知りたい |
fold |
坂を転がりながら大きくなる雪玉 | 自分なりのルールで1つの結果を作りたい |
position |
「Benさんは何番の席?」 | 最初に条件に合う要素のインデックスが欲しい |
find_map |
ドアが開くまで鍵を1本ずつ試す | 各要素で何かを試して、最初に成功したものが欲しい |
partition |
洗濯物を2つのかごに分ける | はい/いいえの質問で、1つのリストを2つに分けたい |
flat_map |
いくつもの袋の中身を1つのテーブルに広げる | 1つの要素が複数の要素になる |
fn main() { let orders = [12, 40, 7, 55, 30];
// sum:単純に足し合わせる let total: i32 = orders.iter().sum();
// fold:自分のルールで足し合わせる(ここでは注文ごとに2ユーロの手数料) let with_fees = orders.iter().fold(0, |acc, price| acc + price + 2); println!("合計 {total}、手数料込み {with_fees}");
// position:最初に条件に合う要素のインデックス println!("50を超える最初の注文のインデックス: {:?}", orders.iter().position(|p| *p > 50));
// partition:「はい」用と「いいえ」用の2つの Vec let (big, small): (Vec<i32>, Vec<i32>) = orders.into_iter().partition(|p| *p >= 30); println!("大口 {big:?}、小口 {small:?}");
// find_map:1つずつ数値に変換してみて、最初に成功したものを使う let inputs = ["abc", "", "42", "7"]; let first_number = inputs.iter().find_map(|s| s.parse::<i32>().ok()); println!("最初の数値: {first_number:?}");
// flat_map:各行がいくつもの単語になり、全部が1つのリストに入る let lines = ["buy milk", "call mum now"]; let words: Vec<&str> = lines.iter().flat_map(|line| line.split(' ')).collect(); println!("{words:?}");}実行結果:
合計 144、手数料込み 15450を超える最初の注文のインデックス: Some(3)大口 [40, 55, 30]、小口 [12, 7]最初の数値: Some(42)["buy", "milk", "call", "mum", "now"]flat_map の例の文字列だけは英語のままにしています。空白で単語に区切る例なので、単語の間に空白を入れない日本語の文では、うまく区切れないからです。
fold は、初期値(0)と、「ここまでの結果」と次の要素を受け取るクロージャを引数にとります。ただ合計したいだけなら、sum のほうが意図がはっきり伝わります。ルールが自分独自のものなら、fold の出番です。
Vec をその場で整理する:dedup と retain
Section titled “Vec をその場で整理する:dedup と retain”この2つはイテレータのメソッドではなく Vec のメソッドですが、同じ引き出しに入れておきたい道具です。dedup は、同じ曲を2回続けては流さないDJのようなものです(あとでまた流すことはあります)。retain は入り口の警備員です。チェックに合格した要素だけが中に残ります。
fn main() { // dedup は、となり合った重複を取り除く let mut clicks = vec!["ホーム", "ホーム", "ショップ", "ショップ", "ショップ", "カート", "ホーム"]; clicks.dedup(); println!("{clicks:?}");
// retain は、条件を満たす要素だけを残す let mut cart = vec![("りんご", 3), ("パン", 0), ("牛乳", 1), ("卵", 0)]; cart.retain(|(_, qty)| *qty > 0); println!("{cart:?}");}実行結果:
["ホーム", "ショップ", "カート", "ホーム"][("りんご", 3), ("牛乳", 1)]最後の "ホーム" が残ったのは、となりに別の "ホーム" がなかったからです。重複をすべて取り除きたいときは、ソートのページで紹介しているように、先に並べ替えておきましょう。
こんなときに使う: 新しい Vec を作らずに、今ある Vec を変更したいとき。
Option と Result の便利メソッド
Section titled “Option と Result の便利メソッド”Option は、届いているかどうかわからない荷物のようなものです。荷物を開けるたびにきちんとした match を書くこともできますが、たいていは一言で済むメソッドが同じことを表してくれます。unwrap_or と match は Option のページで紹介したので、ここではその次に覚えたいメソッドを紹介します。
fn main() { let nickname: Option<String> = None; let age: Option<u32> = Some(17); let width: Option<u32> = Some(3); let height: Option<u32> = Some(4);
// unwrap_or_default:中身か、型のデフォルト値(""、0、空の Vec など) println!("ニックネーム: 「{}」", nickname.unwrap_or_default());
// map_or:None なら代わりの値、そうでなければ値を変換する println!("{}", age.map_or("年齢不明".to_string(), |a| format!("{a}歳")));
// filter:条件を満たすときだけ値を残す println!("成人の年齢: {:?}", age.filter(|a| *a >= 18));
// zip:両方が Some のときだけ Some println!("面積: {:?}", width.zip(height).map(|(w, h)| w * h));
// and_then:それ自体が None になりうる処理をつなげる let good = Some("42").and_then(|s| s.parse::<u32>().ok()); let bad = Some("forty").and_then(|s| s.parse::<u32>().ok()); println!("変換結果: {good:?} と {bad:?}");}実行結果:
ニックネーム: 「」17歳成人の年齢: None面積: Some(12)変換結果: Some(42) と None関数の中では、? は Option にも使えます。値が None なら、関数はその場で None を返します。Result を返す関数の中では、まず ok_or(error) か ok_or_else(|| error) で Option を Result に変えましょう。エラーを作るのに手間がかかるときは ok_or_else を使います。必要なときにしか実行されないからです。
struct Employee { name: String, manager: Option<String>,}
// Option に `?` を使う:None なら、その場で None を返すfn manager_initial(e: &Employee) -> Option<char> { let manager = e.manager.as_ref()?; manager.chars().next()}
// ok_or_else で None をエラーに変えると、Result を返す関数でも `?` が使えるfn port_from(setting: Option<&str>) -> Result<u16, String> { let raw = setting.ok_or_else(|| "PORTが設定されていません".to_string())?; raw.parse::<u16>().map_err(|e| e.to_string())}
fn main() { let ana = Employee { name: "Ana".to_string(), manager: Some("Grace".to_string()) }; let grace = Employee { name: "Grace".to_string(), manager: None }; println!("{}: {:?}", ana.name, manager_initial(&ana)); println!("{}: {:?}", grace.name, manager_initial(&grace));
println!("{:?}", port_from(Some("8080"))); println!("{:?}", port_from(None)); println!("{:?}", port_from(Some("eighty")));
// transpose:Option<Result<..>> が Result<Option<..>> になる let retries_setting: Option<&str> = Some("3"); let retries: Result<Option<u8>, _> = retries_setting.map(|s| s.parse::<u8>()).transpose(); println!("{retries:?}");}実行結果:
Ana: Some('G')Grace: NoneOk(8080)Err("PORTが設定されていません")Err("invalid digit found in string")Ok(Some(3))invalid digit found in string(文字列に無効な数字がある)は、Rust自身が出すエラーメッセージなので英語のままです。
transpose を使う場面は多くありませんが、「設定は省略できて、しかもその解析が失敗するかもしれない」という状況にはぴったりの答えです。これを使えば、結果に ? をつけつつ、Option はそのまま残せます。
こんなときに使う: Option に対する match の、片方の分岐だけが意味のある処理で、もう片方はお決まりの処理になっているとき。
HashMap の entry API
Section titled “HashMap の entry API”クロークの受付を思い浮かべてください。名前を伝えると、係の人はそのフックを見つけるか、新しいフックを用意して、渡してくれます。往復は1回だけです。これが .entry(key) です。ハッシュマップのページでは、定番のカウンターとして entry(key).or_insert(value) を紹介しました。ほかにも覚えておきたいバリエーションが2つあります。
or_insert_with(f)は、キーがないときだけデフォルト値を作ります。or_default()は、同じことを型のデフォルト値で行います。and_modify(f)は、キーがあれば値を変更します。後ろにor_insertをつなげれば、「新しいキー」の場合にも対応できます。
use std::collections::HashMap;
fn main() { let sales = [("りんご", 3), ("なし", 2), ("りんご", 5), ("すもも", 1), ("なし", 4)];
// and_modify + or_insert:あれば更新、なければ追加 let mut totals: HashMap<&str, i32> = HashMap::new(); for (fruit, qty) in sales { totals.entry(fruit).and_modify(|t| *t += qty).or_insert(qty); }
// or_insert_with:空の Vec は必要なときにだけ作る let mut orders: HashMap<&str, Vec<i32>> = HashMap::new(); for (fruit, qty) in sales { orders.entry(fruit).or_insert_with(Vec::new).push(qty); }
// HashMap の順番はランダムなので、表示する前にキーを並べ替える let mut fruits: Vec<&str> = totals.keys().copied().collect(); fruits.sort(); for fruit in fruits { println!("{fruit}: 合計 {}、注文 {:?}", totals[fruit], orders[fruit]); }}実行結果:
すもも: 合計 1、注文 [1]なし: 合計 6、注文 [2, 4]りんご: 合計 8、注文 [3, 5]sort() は文字列を文字コード順に並べるので、ひらがなの場合はおおむね五十音順(す → な → り)になります。
こんなときに使う: 「キーがあれば変更し、なければ追加する」と書きたくなったとき。その書き方だと、キーを2回検索することになります。
never型 !:二度と戻ってこないコード
Section titled “never型 !:二度と戻ってこないコード”一方通行の道を思い浮かべてください。一度入ったら、もう戻ってくることはありません。Rustでは、終わることのない式は never型 を持ち、! と書きます。panic!、return、break、continue、終わらない loop、std::process::exit は、すべてこの型です。
! 型の値は実際には決して存在しないので、コンパイラはこれをどんな型の代わりにも使えるようにしています。だから match で、ある分岐が i32 を返し、別の分岐が return する、という書き方ができるのです。2つ目の分岐は値を作らないので、型の整合性を崩すことがありません。
use std::process;
// `-> !` は「この関数は決して戻らない」という意味fn quit(message: &str) -> ! { println!("{message}"); process::exit(0)}
fn level_points(level: &str) -> u32 { let points: u32 = match level { "easy" => 10, "hard" => 30, _ => return 0, // `return` は `!` 型なので、u32 が期待される場所に置ける }; points * 2}
fn main() { println!("easy: {}、hard: {}、weird: {}", level_points("easy"), level_points("hard"), level_points("weird"));
let mut total = 0; for input in ["4", "oops", "6"] { let n: i32 = match input.parse() { Ok(n) => n, Err(_) => continue, // `continue` も `!` 型 }; total += n; } println!("合計: {total}");
let config_file: Option<&str> = None; let config = match config_file { Some(path) => path, None => quit("設定ファイルが見つからないので終了します"), }; println!("{config} を読み込みました"); // 表示されることはない}実行結果:
easy: 20、hard: 60、weird: 0合計: 10設定ファイルが見つからないので終了します1つ注意があります。process::exit はプログラムをその場で止めてしまい、まだ生きている値の後片付けのコード(デストラクタ)を実行しません。実際のプログラムでは、main から普通に戻るほうが穏やかな終わり方です。
コードの下書き中に、プレースホルダーとして ! を使うマクロが2つあります。todo!() は「あとで書きます」、unimplemented!() は「ここには対応していません」という意味です。どちらも実際に到達するとパニックしますが、それ以外の部分はコンパイルできるようにしてくれます。
fn export_pdf(report: &str) -> Vec<u8> { todo!("PDF出力はv2で対応予定")}これを呼び出すと not yet implemented: PDF出力はv2で対応予定 と表示されます。unimplemented!("PDF出力") なら、メッセージは not implemented: PDF出力 です。前半の英語の部分は、Rust自身が出すメッセージです。
こんなときに使う: 関数でプログラムを止めたいとき。または、ある分岐で値を返す代わりに早めに抜けたいとき。
ちょっとした便利技
Section titled “ちょっとした便利技”ポケットに入れておく道具のようなものです。小さいけれど、毎週のように使います。
matches!(value, pattern)は「この値はパターンに合う?」にtrueかfalseで答えます(詳しくはパターンマッチングのページで)。std::cmp::min/maxは2つの値のうち小さいほう・大きいほうを選び、.clamp(low, high)は値を範囲内に収めます。100で止まる音量つまみのようなものです。.abs_diff(other)は2つの数の差(距離)を返します。結果は常にプラスで、符号なし整数型でもオーバーフローしません。dbg!(expr)はファイル名、行番号、式、値を表示してから、その値をそのまま返します。だから、どんな式でも包み込めます。assert!(condition)は条件が false ならパニックします。debug_assert!も同じですが、デバッグビルドでしかチェックしないので、リリースビルドでは何のコストもかかりません。
use std::cmp::{max, min};
enum Order { Pending, Shipped, Delivered, Cancelled,}
fn main() { let orders = [Order::Pending, Order::Delivered, Order::Shipped, Order::Cancelled, Order::Pending];
// matches!:はい/いいえで答えるパターンチェック。| は「または」 let open = orders.iter().filter(|o| matches!(o, Order::Pending | Order::Shipped)).count(); println!("未完了の注文: {open}");
// min、max、clamp println!("min {}、max {}", min(7, 3), max(7, 3)); let volume = 140; let safe = volume.clamp(0, 100); println!("音量 {volume} を {safe} に制限");
// abs_diff:どれだけ離れているか。マイナスにはならない let (guess, secret): (u32, u32) = (30, 42); println!("正解まであと {}", guess.abs_diff(secret));
// dbg! は標準エラー出力に表示して、値を返す let doubled = dbg!(safe * 2);
// assert! は、これが false になったらプログラムを止める assert!(doubled <= 200, "2倍にした音量が大きすぎます: {doubled}"); debug_assert!(safe <= 100); // デバッグビルドでだけチェックされる println!("すべてのチェックに合格");}実行結果:
未完了の注文: 3min 3、max 7音量 140 を 100 に制限正解まであと 12すべてのチェックに合格上の実行結果に dbg! の行が出てこないのは、dbg! が標準出力(stdout)ではなく 標準エラー出力 (stderr)に書き込むからです。Playgroundでは「Standard Error」の欄に表示されます。
[src/main.rs:28:19] safe * 2 = 200だからこそ、デバッグ中に dbg! をあちこちに入れても安心です。プログラム本来の出力に混ざらないからです。ただし、公開する前には忘れずに消しておきましょう。
ソートにも小さな道具がそろっています。詳しくはソートのページで説明しているので、ここでは要点だけ紹介します。sort_unstable は sort より少し速く、単純な数値ならこれで十分です。sort_by_key は1つのフィールドで並べ替えます。binary_search は ソート済み のデータから要素をとても速く見つけます。辞書を1ページ目から読むのではなく、真ん中あたりから開いて探すようなものです。
struct Player { name: &'static str, score: u32,}
fn main() { // sort_unstable:少し速い。等しい要素の順番は入れ替わることがある let mut ids = vec![42, 7, 19, 3, 25]; ids.sort_unstable(); println!("{ids:?}");
// binary_search:見つかれば Ok(インデックス)、なければ Err(入るべき位置) println!("19を探す: {:?}", ids.binary_search(&19)); println!("20を探す: {:?}", ids.binary_search(&20));
// sort_by_key:1つのフィールドで並べ替える let mut players = vec![ Player { name: "Ana", score: 120 }, Player { name: "Ben", score: 95 }, Player { name: "Chen", score: 150 }, ]; players.sort_by_key(|p| p.score); let names: Vec<&str> = players.iter().map(|p| p.name).collect(); println!("低い順: {names:?}");}実行結果:
[3, 7, 19, 25, 42]19を探す: Ok(2)20を探す: Err(3)低い順: ["Ben", "Ana", "Chen"]Err(3) は、心配するような失敗ではありません。20がリストにないこと、そしてリストをソートされた状態に保つにはインデックス3に挿入すればよいことを教えてくれています。
こんなときに使う: ちょっとした if やループを書いていて、それがこれらの道具でできることだと気づいたとき。
よくある間違い
Section titled “よくある間違い”Default を実装していない型に mem::take を使う。 mem::take は代わりに 何か を残さなければならないので、その型にとっての「空」が何なのかを知っている必要があります。
use std::mem;
struct Connection { host: String,}
struct Pool { active: Connection,}
impl Pool { fn reset(&mut self) -> Connection { mem::take(&mut self.active) // ❌ 「空の」Connection って何? }}error[E0277]: the trait bound `Connection: Default` is not satisfied --> src/main.rs:13:19 |13 | mem::take(&mut self.active) // ❌ 「空の」Connection って何? | --------- ^^^^^^^^^^^^^^^^ the trait `Default` is not implemented for `Connection` | | | required by a bound introduced by this call |help: consider annotating `Connection` with `#[derive(Default)]`コンパイラは「Connection には Default トレイトが実装されていません」と言っていて、#[derive(Default)] をつけることを提案しています。直し方の1つは、mem::replace(&mut self.active, Connection { host: "db2".to_string() }) のように、代わりに入れる値を自分で選ぶことです。もう1つは、フィールドを Option<Connection> にして self.active.take() を使うことです。#[derive(Default)] を追加するのは、その型にとって「空」の値が本当に意味を持つときだけにしましょう。
Result を返す関数の中で、Option に ? を使う。 この2つは自動的には混ぜられません。
fn port_from(setting: Option<&str>) -> Result<u16, String> { let raw = setting?; // ❌ None には返すべきエラーメッセージがない raw.parse::<u16>().map_err(|e| e.to_string())}error[E0277]: the `?` operator can only be used on `Result`s, not `Option`s, in a function that returns `Result` --> src/main.rs:2:22 |1 | fn port_from(setting: Option<&str>) -> Result<u16, String> { | ---------------------------------------------------------- this function returns a `Result`2 | let raw = setting?; // ❌ None には返すべきエラーメッセージがない | ^ use `.ok_or(...)?` to provide an error compatible with `Result<u16, String>`「Result を返す関数では、? 演算子は Option ではなく Result にしか使えません」という意味です。直し方はコンパイラがもう教えてくれています。setting.ok_or("PORTが設定されていません".to_string())? と書くか、上の例のように ok_or_else を使いましょう。このエラーのほかの形については E0277 を見てください。
ソートされていないデータに dedup や binary_search を使う。 dedup はとなり合った重複しか取り除きませんし、ソートされていないデータに binary_search を使っても意味のない答えが返ってきます。どちらもコンパイルエラーにはならないので、見落としやすいのです。先にソートしましょう。
chunks_exact の余りを忘れる。 chunks_exact(3) を for ループで回すと、最後の1個か2個は何も言わずに飛ばされます。それが大事なデータなら .remainder() を処理するか、代わりに chunks を使いましょう。
1. 毎日の値動き
Section titled “1. 毎日の値動き”価格 [100, 104, 98, 110, 107] があります。ある日から次の日への変化を集めた Vec を作って表示し、続けていちばん大きな値上がりを表示してください。ヒント: windows(2) は各ペアを小さなスライス w として渡してくれるので、変化は w[1] - w[0] です。いちばん大きい値は .iter().max() で見つかります。
解答を見る
fn main() { let prices = [100, 104, 98, 110, 107];
let changes: Vec<i32> = prices.windows(2).map(|w| w[1] - w[0]).collect(); println!("変化: {changes:?}");
let biggest_rise = changes.iter().max(); println!("最大の値上がり: {biggest_rise:?}");}実行結果:
変化: [4, -6, 12, -3]最大の値上がり: Some(12)価格が5つなら、変化は4つです。windows(2) は1歩進むごとに1つのペアを作るからです。max() が Option を返すのは、空のリストには最大値がないからです。
2. 印刷キューを空にする
Section titled “2. 印刷キューを空にする”queue: Vec<String> フィールドを持つ Printer 構造体を書いてください。メソッドとして、add(&mut self, file: &str) と、キューにたまったファイルをすべて返してキューを空にする flush(&mut self) -> Vec<String> を用意します。ファイルを2つ追加してから flush し、取り出したファイルと、キューに残っているファイルの数を表示してください。ヒント: flush は &mut self しか持っていないので、std::mem::take を使います。
解答を見る
use std::mem;
struct Printer { queue: Vec<String>,}
impl Printer { fn add(&mut self, file: &str) { self.queue.push(file.to_string()); }
fn flush(&mut self) -> Vec<String> { mem::take(&mut self.queue) }}
fn main() { let mut printer = Printer { queue: Vec::new() }; printer.add("report.pdf"); printer.add("photo.png");
let jobs = printer.flush(); println!("{}件を印刷: {jobs:?}", jobs.len()); println!("キューの残り: {}", printer.queue.len());}実行結果:
2件を印刷: ["report.pdf", "photo.png"]キューの残り: 0flush の中で self.queue とだけ書くと、E0507 になってしまいます。mem::take なら、コピーせずに Vec をムーブで取り出し、代わりに空の Vec を残してくれます。
3. 最初の有効なポート番号
Section titled “3. 最初の有効なポート番号”u16 として解析できる最初の文字列を返す関数 first_port(candidates: &[&str]) -> Option<u16> を書いてください。["", "abc", "70000", "8080", "9090"] と ["none", "-1"] でそれぞれ呼び出し、ポート番号を表示します。見つからないときは 3000 を表示してください。ヒント: s.parse::<u16>().ok() を find_map で使い、最後に unwrap_or(3000) をつけます。
解答を見る
fn first_port(candidates: &[&str]) -> Option<u16> { candidates.iter().find_map(|s| s.parse::<u16>().ok())}
fn main() { let from_config = ["", "abc", "70000", "8080", "9090"]; let all_bad = ["none", "-1"];
println!("使用するポート: {}", first_port(&from_config).unwrap_or(3000)); println!("使用するポート: {}", first_port(&all_bad).unwrap_or(3000));}実行結果:
使用するポート: 8080使用するポート: 3000"70000" は数値に見えますが、u16 には収まりません(最大は65535)。そのため解析に失敗し、find_map は次へ進みます。"8080" で止まるので、"9090" は見られることすらありません。
mem::take、mem::replace、Option::takeは、代わりに何かを残すことで&mutから値を取り出します。mem::swapは2つの値を交換します。- スライスでは、
windowsは重なり合うとなり同士を、chunksはまとまりを、split_first/split_lastは「1つの要素と残り」を返します。 peekableを使えば先をのぞけます。zip、enumerate、chain、fold、find_map、partitionは、手書きのループの多くを置き換えてくれます。unwrap_or_default、map_or、filter、ok_orのようなOptionの便利メソッドを使えば、小さなmatchを書かずに済みます。?はOptionにも使えます。entry()にor_insert_withやand_modifyを組み合わせると、1回の検索でHashMapを更新できます。- never型
!は、終わることのないコード(panic!、return、continue、process::exit)の型です。だから、値が期待されるどんな場所にも置けます。
理解度チェック
Section titled “理解度チェック”理解度チェック
1. items が Vec のとき、mem::take(&mut self.items) は何を残しますか?
2. [1, 2, 3, 4].windows(2) は何を返しますか?
3. let x: i32 = match s { "a" => 1, _ => return }; がコンパイルできるのはなぜですか?
4. 関数は Result を返しますが、手元にあるのは Option です。? の前に何をつけますか?
スコア: 0 / 4
さらに詳しく
Section titled “さらに詳しく”std::mem— 標準ライブラリのドキュメント(英語):take、replace、swapなどのメモリ関連の関数を、例とともに紹介しています。- スライスのメソッド — 標準ライブラリのドキュメント(英語):
windows、chunks、split_first、binary_searchを含む、スライスのすべてのメソッドが載っています。 Iteratorトレイト — 標準ライブラリのドキュメント(英語):zipやpeekableからpartition、find_mapまで、イテレータのメソッドの一覧です。Option— 標準ライブラリのドキュメント(英語):Optionの便利メソッドが、用途ごとにまとめられています。- 発散する関数 — Rust By Example 日本語版:
!を返す関数の短い例と、それが役に立つ理由を紹介しています。