Rust練習問題
Rustについて読むのは役に立ちます。でも、身につくのは実際にRustを書いたときです。このページには小さな問題が18問あります。テーマごとに分けて、やさしいものから難しいものへ順に並べました。
それぞれの問題では、まず課題を読んで自分で挑戦してみてください。解答を開くのはそのあとです。行き詰まったり、コンパイラのエラーが出たりしてもまったく問題ありません。それも学習のうちですし、Rustのエラーメッセージはたいてい最高のヒントになります。
練習のしかた:別のタブで Rust Playground を開くか、cargo new practice でプロジェクトを作りましょう(Rustを始めるを参照)。そこで答えを書いて、解答と見比べます。書き方が違っても同じ結果が表示されるなら、たぶん大丈夫です。良いやり方はほとんどの場合ひとつではありません。
1. FizzBuzz
Section titled “1. FizzBuzz”1から15までの数を表示します。ただし、3の倍数のときは Fizz、5の倍数のときは Buzz、3と5の両方の倍数のときは FizzBuzz と表示してください。
ヒント:1..=15 を回す for ループと、%(余り)演算子を使います。「両方の倍数」を最初にチェックしましょう。
解答を見る
fn main() { for n in 1..=15 { if n % 15 == 0 { println!("FizzBuzz"); } else if n % 3 == 0 { println!("Fizz"); } else if n % 5 == 0 { println!("Buzz"); } else { println!("{n}"); } }}実行結果:
12Fizz4BuzzFizz78FizzBuzz11Fizz1314FizzBuzzチェックの順番が大事です。先に n % 3 == 0 を調べると、15のときに Fizz と表示されてしまい、FizzBuzz の分岐までたどり着きません。3でも5でも割り切れる数は、15で割り切れる数です。
2. 文字列を逆にする
Section titled “2. 文字列を逆にする”文字列を逆順にして返す関数 reverse(s: &str) -> String を書いてください。"stressed" は "desserts" になります。
ヒント:.chars() で文字を1つずつ取り出せます。.rev() はイテレータを逆順にし、.collect() で String を組み立てられます。
解答を見る
fn reverse(s: &str) -> String { s.chars().rev().collect()}
fn main() { println!("{}", reverse("stressed")); println!("{}", reverse("hello")); println!("{}", reverse("año"));}実行結果:
dessertsollehoñaなぜバイトではなく .chars() を使うのでしょうか。Rustの String はUTF-8なので、ñ のような文字は2バイトを使います(日本語の文字なら3バイトです)。バイト単位で逆にすると、文字が壊れて不正なテキストになってしまいます。.chars() なら文字をまるごと扱えます。くわしくは文字列を見てください。
3. 母音を数える
Section titled “3. 母音を数える”文字列に含まれる母音(a, e, i, o, u)を数える count_vowels(s: &str) -> usize を書いてください。小文字も大文字も数えます。
ヒント:.chars().filter(...).count() を使います。"aeiouAEIOU" のような文字列には .contains(c) メソッドがあるので、各文字が母音かどうかを調べるのに使えます。
解答を見る
fn count_vowels(s: &str) -> usize { s.chars().filter(|c| "aeiouAEIOU".contains(*c)).count()}
fn main() { println!("{}", count_vowels("Hello, World")); println!("{}", count_vowels("Rust is fun")); println!("{}", count_vowels("AEIOU and sometimes y")); println!("{}", count_vowels("rhythm"));}実行結果:
33100filter はクロージャに各要素への参照(&char)を渡します。そのため、char そのものを取り出すために *c と書いています。このパターンはクロージャとイテレータのページで説明しています。
所有権と借用
Section titled “所有権と借用”4. ムーブのエラーを直す
Section titled “4. ムーブのエラーを直す”このプログラムはコンパイルできません。shout 関数の引数と、2か所の呼び出しだけを変えて、HELLO! が2回表示されるようにしてください。
fn shout(text: String) { println!("{}!", text.to_uppercase());}
fn main() { let word = String::from("hello"); shout(word); shout(word);}error[E0382]: use of moved value: `word` --> src/main.rs:8:11 |6 | let word = String::from("hello"); | ---- move occurs because `word` has type `String`, which does not implement the `Copy` trait7 | shout(word); | ---- value moved here8 | shout(word); | ^^^^ value used here after moveコンパイラは「ムーブ済みの値 word を使っている」と言っています。1回目の shout(word) で値がムーブされ、2回目で使えなくなっているということです。
ヒント:shout はテキストを見るだけでよく、所有する必要はありません。所有権を参照してください。
解答を見る
fn shout(text: &str) { println!("{}!", text.to_uppercase());}
fn main() { let word = String::from("hello"); shout(&word); shout(&word);}実行結果:
HELLO!HELLO!関数が String を受け取ると、呼び出したときに文字列が関数の中へムーブされ、main ではもう使えなくなります。&str を受け取るようにすれば、関数は借用するだけで済みます。コンパイラは word.clone() も提案してきます。それでも動きますが、意味もなく文字列をまるごとコピーすることになります。ここでは借用で直すのがベストです。
5. 借用のエラーを直す
Section titled “5. 借用のエラーを直す”このプログラムもコンパイルできません。リストに Ben を追加したまま、Ana と表示されるようにしてください。
fn main() { let mut names = vec![String::from("Ana")]; let first = &names[0]; names.push(String::from("Ben")); println!("{first}");}error[E0502]: cannot borrow `names` as mutable because it is also borrowed as immutable --> src/main.rs:4:5 |3 | let first = &names[0]; | ----- immutable borrow occurs here4 | names.push(String::from("Ben")); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here5 | println!("{first}"); | ----- immutable borrow later used hereこのエラーは「names はすでに不変で借用されているので、可変で借用できない」という意味です。
ヒント:問題は順番です。push がベクタを変更するとき、first はまだ使われています。借用を参照してください。
解答を見る
fn main() { let mut names = vec![String::from("Ana")]; names.push(String::from("Ben")); let first = &names[0]; println!("{first}");}実行結果:
AnaなぜRustはここまで厳しいのでしょうか。push で場所が足りなくなると、ベクタはすべての要素をメモリ上の新しい、もっと広い場所へ移すことがあります。そうなると first は古い場所を指したままになり、その場所はもう有効ではありません。Rustのルール(読み手はいくつでも、または書き手は1つだけ。両方を同時には持てない)は、このバグをプログラムの実行前に防いでくれます。push のあとで参照を取れば解決です。
6. 文字列の最初の単語
Section titled “6. 文字列の最初の単語”最初の単語(最初のスペースより前の部分)を返す first_word(s: &str) -> &str を書いてください。スペースがなければ文字列全体を返します。新しい String は作らずに、元の文字列のスライスを返してください。
ヒント:s.find(' ') は最初のスペースの位置を Option<usize> で返します。&s[..i] は先頭から i までのスライスです。
解答を見る
fn first_word(s: &str) -> &str { match s.find(' ') { Some(i) => &s[..i], None => s, }}
fn main() { println!("{:?}", first_word("hello world")); println!("{:?}", first_word("Rust")); println!("{:?}", first_word(""));}実行結果:
"hello""Rust"""返される &str は入力と同じメモリを指しているので、何もコピーされません。コンパイラは、結果が入力より長く生きられないことを知っています。これがライフタイムの省略の働きです。参照を1つ受け取って1つ返すなら、ライフタイムを自分で書く必要はありません。
もっと短く書くなら s.split_whitespace().next().unwrap_or("") です。こちらは先頭の余分なスペースも読み飛ばします。
(ここでは文字列を英語のままにしています。スライスはバイト位置で切り出すので、1文字が3バイトの日本語だと位置の考え方が変わってしまうからです。)
コレクション
Section titled “コレクション”7. ベクタの中で最大の数
Section titled “7. ベクタの中で最大の数”largest(numbers: &[i32]) -> Option<i32> を書いてください。いちばん大きな数を返し、リストが空なら None を返します。今回は組み込みの .max() を使わず、ループを自分で書きましょう。
ヒント:最初の要素を「今のところ最大」として始めます。numbers.first() は Option を返すので、空の場合もこれで扱えます。
解答を見る
fn largest(numbers: &[i32]) -> Option<i32> { let mut biggest = *numbers.first()?; for &n in numbers { if n > biggest { biggest = n; } } Some(biggest)}
fn main() { println!("{:?}", largest(&[3, 41, 7, 12])); println!("{:?}", largest(&[-5, -2, -9])); println!("{:?}", largest(&[]));}実行結果:
Some(41)Some(-2)Nonenumbers.first()? は、リストが空ならその場で None を返します。関数が Option を返すのはそのためです。「何もない中の最大値」には良い答えがないので、Rustはそれをはっきり書かせます。実際のコードでは numbers.iter().max() が同じことをしてくれます。
最初の要素ではなく 0 から始めるのは典型的なバグです。[-5, -2, -9] というリストに対して、間違って 0 を返してしまいます。
8. 単語の出現回数
Section titled “8. 単語の出現回数”文章の中で、それぞれの単語が何回出てくるかを数えてください。大文字・小文字の違いと句読点は無視し、単語をアルファベット順に表示します。
ヒント:BTreeMap<String, usize> を使いましょう。HashMap と同じように使えますが、キーを並べ替えた状態で保ちます。map.entry(word).or_insert(0) で、1を足すためのカウンターが手に入ります。
解答を見る
use std::collections::BTreeMap;
fn main() { let text = "The cat sat on the mat. The end!"; let mut counts: BTreeMap<String, usize> = BTreeMap::new();
for word in text.split_whitespace() { let clean = word .trim_matches(|c: char| !c.is_alphanumeric()) .to_lowercase(); if clean.is_empty() { continue; } *counts.entry(clean).or_insert(0) += 1; }
for (word, count) in &counts { println!("{word}: {count}"); }}実行結果:
cat: 1end: 1mat: 1on: 1sat: 1the: 3なぜ HashMap ではなく BTreeMap なのでしょうか。HashMap は要素を予測できない順番で返し、その順番は実行するたびに変わることもあります。並べ替えた結果が必要なら BTreeMap を使うか、HashMap を Vec に集めてからソートしましょう。
trim_matches は mat. から . を、end! から ! を取り除きます。to_lowercase によって The と the が同じ単語になります。
(この例の文章が英語のままなのは、スペースで単語を区切っているからです。日本語の文章は単語の間にスペースを入れないので、この方法では分けられません。)
9. 偶数の2乗
Section titled “9. 偶数の2乗”vec![1, 2, 3, 4, 5, 6, 7, 8] から、偶数だけを2乗した新しいベクタ [4, 16, 36, 64] を作ってください。そのあと合計を表示します。for ループではなく、イテレータのメソッドを使いましょう。
ヒント:.iter()、.filter(...)、.map(...)、.collect() をつなげます。そのあと .iter().sum() で合計できます。
解答を見る
fn main() { let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8];
let squares: Vec<i32> = numbers .iter() .filter(|n| *n % 2 == 0) .map(|n| n * n) .collect();
let total: i32 = squares.iter().sum();
println!("{:?}", squares); println!("合計: {total}");}実行結果:
[4, 16, 36, 64]合計: 120このチェーンは、レシピのように上から下へ読んでいきます。数を取り出し、偶数だけを残し、それぞれを2乗して、Vec に入れる。イテレータは遅延評価なので、.collect() が結果を求めるまで実際には何も動きません。イテレータを参照してください。
10. 数を読み取って合計する
Section titled “10. 数を読み取って合計する”sum_numbers(input: &str) -> Result<i32, ParseIntError> を書いてください。"1, 2, 3" のようなテキストを受け取り、それぞれの数を読み取って合計を返します。数ではない部分があったら、クラッシュせずにエラーを返してください。
ヒント:',' で分割し、それぞれを .trim() してから .parse() します。エラーのときにすぐ返すには ? 演算子を使います。
解答を見る
use std::num::ParseIntError;
fn sum_numbers(input: &str) -> Result<i32, ParseIntError> { let mut total = 0; for piece in input.split(',') { let n: i32 = piece.trim().parse()?; total += n; } Ok(total)}
fn main() { for input in ["1, 2, 3", "10,20, 30", "4, five, 6", ""] { match sum_numbers(input) { Ok(sum) => println!("{input:?} -> 合計は {sum}"), Err(e) => println!("{input:?} -> エラー: {e}"), } }}実行結果:
"1, 2, 3" -> 合計は 6"10,20, 30" -> 合計は 60"4, five, 6" -> エラー: invalid digit found in string"" -> エラー: cannot parse integer from empty stringエラーメッセージの部分はRust自身が出すものなので英語です。invalid digit found in string は「文字列に数字ではない文字があった」、cannot parse integer from empty string は「空の文字列は整数として読み取れない」という意味です。
? はたった1文字で2つの仕事をします。parse() が Ok(n) を返せば n が手に入ります。Err(e) を返せば、関数はその場で Err(e) を返します。だからこの関数も Result を返す必要があるのです。
おまけ: Result は .sum() とも組み合わせられます。次の1行でも同じことができ、最初のエラーで止まります。
input.split(',').map(|p| p.trim().parse::<i32>()).sum()11. Option のチェーン
Section titled “11. Option のチェーン”ユーザーはメールアドレスを持っているかもしれないし、持っていないかもしれません。@ より後ろの部分を返す email_domain(user: &User) -> Option<&str> を書いてください。メールアドレスがない場合や、メールアドレスに @ がない場合は None を返します。そのあと、名前でユーザーを探してドメインを表示しましょう。
ヒント:user.email.as_deref() は Option<String> を Option<&str> に変えます。email.split_once('@') は Option<(&str, &str)> を返します。? 演算子は Option にも使えます。Optionを参照してください。
解答を見る
struct User { name: String, email: Option<String>,}
fn email_domain(user: &User) -> Option<&str> { let email = user.email.as_deref()?; let (_, domain) = email.split_once('@')?; Some(domain)}
fn main() { let users = vec![ User { name: String::from("Ben"), email: None }, User { name: String::from("Cleo"), email: Some(String::from("cleo-at-home")) }, ];
for name in ["Ana", "Ben", "Cleo", "Dan"] { let domain = users .iter() .find(|u| u.name == name) .and_then(email_domain) .unwrap_or("(ドメインなし)"); println!("{name}: {domain}"); }}実行結果:
Ana: example.comBen: (ドメインなし)Cleo: (ドメインなし)Dan: (ドメインなし)ここでは「失敗」のしかたが3通りあります。Danは存在しない、Benにはメールアドレスがない、Cleoのメールアドレスには @ がない。どれも Option の1ステップで、? や .and_then() は最初の None で止まります。if でのチェックも、ヌルポインタのエラーもありません。最後の .unwrap_or(...) がデフォルト値を用意してくれます。
12. 銀行口座
Section titled “12. 銀行口座”持ち主(owner)と残高(balance)を持つ Account 構造体を作ってください。deposit(amount) と withdraw(amount) を用意します。withdraw は Result<u64, String> を返す必要があります。うまくいったら新しい残高を、お金が足りなければエラーメッセージを返します。残高が0より下になってはいけません。
ヒント:口座を変更するメソッドは &mut self を受け取ります。引き算の前に残高をチェックしましょう。構造体とResultを参照してください。
解答を見る
struct Account { owner: String, balance: u64,}
impl Account { fn new(owner: &str) -> Account { Account { owner: owner.to_string(), balance: 0 } }
fn deposit(&mut self, amount: u64) { self.balance += amount; }
fn withdraw(&mut self, amount: u64) -> Result<u64, String> { if amount > self.balance { return Err(format!( "{amount} は引き出せません: 残高は {} しかありません", self.balance )); } self.balance -= amount; Ok(self.balance) }}
fn main() { let mut account = Account::new("Ana"); account.deposit(100);
for amount in [30, 50, 40] { match account.withdraw(amount) { Ok(left) => println!("{amount} を引き出しました。残りは {left} です"), Err(e) => println!("エラー: {e}"), } }
println!("最終的に {} の残高は {} です", account.owner, account.balance);}実行結果:
30 を引き出しました。残りは 70 です50 を引き出しました。残りは 20 ですエラー: 40 は引き出せません: 残高は 20 しかありません最終的に Ana の残高は 20 ですu64(符号なしの数)を使うと、さらに安全になります。そもそも負の残高を持つことができないからです。もしチェックを忘れても、デバッグモードでは self.balance -= amount が “attempt to subtract with overflow”(引き算でオーバーフローした)というメッセージでパニックします。黙ってマイナスになることはありません。
トレイトとジェネリクス
Section titled “トレイトとジェネリクス”13. Shape トレイト
Section titled “13. Shape トレイト”area(&self) -> f64 と name(&self) -> String の2つのメソッドを持つトレイト Shape を定義してください。それを Circle(半径を持つ)と Rectangle(幅と高さを持つ)に実装します。両方を1つの Vec に入れて、それぞれの名前と面積を小数点以下2桁で表示しましょう。
ヒント:異なる型を1つの Vec に入れるには Vec<Box<dyn Shape>> を使います。トレイトとトレイトオブジェクトを参照してください。{:.2} で小数点以下2桁を表示できます。
解答を見る
trait Shape { fn area(&self) -> f64; fn name(&self) -> String;}
struct Circle { radius: f64,}
struct Rectangle { width: f64, height: f64,}
impl Shape for Circle { fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius } fn name(&self) -> String { format!("円 (r = {})", self.radius) }}
impl Shape for Rectangle { fn area(&self) -> f64 { self.width * self.height } fn name(&self) -> String { format!("長方形 ({} x {})", self.width, self.height) }}
fn main() { let shapes: Vec<Box<dyn Shape>> = vec![ Box::new(Circle { radius: 1.0 }), Box::new(Rectangle { width: 3.0, height: 4.0 }), Box::new(Circle { radius: 2.5 }), ];
let mut total = 0.0; for shape in &shapes { println!("{}: {:.2}", shape.name(), shape.area()); total += shape.area(); } println!("面積の合計: {total:.2}");}実行結果:
円 (r = 1): 3.14長方形 (3 x 4): 12.00円 (r = 2.5): 19.63面積の合計: 34.78ループは、自分が扱っているのがどの図形なのかを知りませんし、気にもしません。わかっているのは、それぞれの要素が「Shape である」ことだけです。だから area() と name() を呼び出せます。あとで Triangle を追加するときも、新しい impl を1つ書くだけで、ループはまったく変えなくて済みます。
14. ジェネリックな largest
Section titled “14. ジェネリックな largest”問題7に戻って、比較できるどんな型でも使えるようにしてください。整数、小数、文字、文字列のどれでも動くようにします。シグネチャは largest<T: PartialOrd>(items: &[T]) -> Option<&T> です。
ヒント:T: PartialOrd は「T は < と > が使える」という意味です。参照(&T)を返せば、要素をコピーしたりクローンしたりする必要がありません。ジェネリクスを参照してください。
解答を見る
fn largest<T: PartialOrd>(items: &[T]) -> Option<&T> { let mut biggest = items.first()?; for item in items { if item > biggest { biggest = item; } } Some(biggest)}
fn main() { println!("{:?}", largest(&[3, 41, 7, 12])); println!("{:?}", largest(&[2.5, -1.0, 9.75])); println!("{:?}", largest(&['r', 'u', 's', 't'])); println!("{:?}", largest(&["pear", "apple", "zucchini"]));
let empty: Vec<i32> = Vec::new(); println!("{:?}", largest(&empty));}実行結果:
Some(41)Some(9.75)Some('u')Some("zucchini")None本体は問題7とほとんど同じです。本当に変わったのは <T: PartialOrd> の部分だけです。この制約(トレイト境界)がないと、すべての型が比較できるわけではないので、コンパイラは item > biggest を受け付けません。文字列はアルファベット順で比較されるので、"zucchini" が最大になります。
空のベクタに Vec<i32> と書いている点にも注目してください。要素が1つもないと、Rustは T が何なのかを推測できないので、教えてあげる必要があります。
セット・並べ替え・パターン・イテレータ
Section titled “セット・並べ替え・パターン・イテレータ”15. 重複を見つける
Section titled “15. 重複を見つける”招待客リストに、何度も書かれている名前があります。2回以上出てくる名前をすべて、1つにつき1回だけ、アルファベット順で返す find_repeats(words: &[&str]) -> Vec<String> を書いてください。あわせて、重複を除いた招待客の人数も表示しましょう。
ヒント:HashSet を使えば、すでに見た値を覚えておけます。insert メソッドは、値がすでに入っていたときに false を返します。HashSet には決まった順序がないので、最後に結果を並べ替えましょう。
解答を見る
use std::collections::HashSet;
fn find_repeats(words: &[&str]) -> Vec<String> { let mut seen = HashSet::new(); let mut repeats = HashSet::new();
for &word in words { // insert は、その単語がすでにセットにあれば false を返す if !seen.insert(word) { repeats.insert(word); } }
let mut result: Vec<String> = repeats.into_iter().map(String::from).collect(); result.sort(); result}
fn main() { let guests = ["ana", "ben", "cleo", "ben", "dan", "ana", "ben"]; println!("重複: {:?}", find_repeats(&guests));
let unique: HashSet<&str> = guests.iter().copied().collect(); println!("招待客の人数(重複なし): {}", unique.len());
println!("重複: {:?}", find_repeats(&["x", "y", "z"]));}実行結果:
重複: ["ana", "ben"]招待客の人数(重複なし): 4重複: []なぜセットが2つあるのでしょうか。seen には、それまでに出てきた名前がすべて入ります。repeats には、2回目に出会った名前が入ります。"ben" は3回出てきますが、repeats はセットなので、保存されるのは1回だけです。
HashSet に collect するのは、重複しない値を数えるいちばん手っ取り早い方法でもあります。重複は自然に消えてしまうからです。result.sort() の行がないと、重複した名前の順番が実行するたびに変わることがあります。
16. 単語を長さ順に並べる
Section titled “16. 単語を長さ順に並べる”単語のリストを、短いものから長いものへ並べ替えてください。長さが同じ単語どうしは、大文字・小文字を区別せずに、AからZの順に並べます。そのあと、ふつうの .sort() の結果と見比べてみましょう。
ヒント:sort_by は、2つの要素を比べて Ordering を返すクロージャを受け取ります。.then_with(...) を使うと、同点のときだけ効く2つ目の比較を追加できます。並べ替えと検索を参照してください。
解答を見る
fn main() { let mut words = vec!["pear", "Fig", "banana", "apple", "kiwi", "Cherry", "date"];
let mut plain = words.clone(); plain.sort(); println!("ふつうのソート: {plain:?}");
words.sort_by(|a, b| { a.len() .cmp(&b.len()) .then_with(|| a.to_lowercase().cmp(&b.to_lowercase())) }); println!("長さ順: {words:?}");}実行結果:
ふつうのソート: ["Cherry", "Fig", "apple", "banana", "date", "kiwi", "pear"]長さ順: ["Fig", "date", "kiwi", "pear", "apple", "banana", "Cherry"]この問題の単語は英語のままにしています。.len() はバイト数を返すので、日本語の単語(1文字3バイト)だと、見た目の文字数とは違う順番になってしまうからです。
ふつうのソートの結果を見てください。"Cherry" と "Fig" が先頭に来ています。文字列は文字コードで比べられ、大文字はすべて小文字より前に来るからです。だから、同点のときの比較では to_lowercase() したものどうしを比べています。
同じルールは、words.sort_by_key(|w| (w.len(), w.to_lowercase())) と短く書くこともできます。タプルは要素を1つずつ順に比べるので、まず長さで決まり、同じ長さなら小文字にした単語で決まります。
17. マッチガードで送料を計算する
Section titled “17. マッチガードで送料を計算する”次の列挙型について、match を1つだけ使って shipping_cost(p: Package) -> String を書いてください。
enum Package { Letter { grams: u32 }, Parcel { kg: u32, express: bool }, Pickup,}ルールは次のとおりです。店頭受け取りは無料です。手紙は20 gまでなら$1、それより重ければ$3です。30 kgを超える小包はそもそも発送できません。速達の小包は$10に1 kgあたり$2を足した額、それ以外の小包は$5に1 kgあたり$1を足した額です。
ヒント:match のアームでは、パターンのあとに if を書けます。これをガードと呼びます(Package::Letter { grams } if grams <= 20 => ...)。アームは上から順にチェックされるので、「重すぎる」ルールは速達のルールより前に置きましょう。パターンマッチングを参照してください。
解答を見る
enum Package { Letter { grams: u32 }, Parcel { kg: u32, express: bool }, Pickup,}
fn shipping_cost(p: Package) -> String { match p { Package::Pickup => String::from("無料(店頭受け取り)"), Package::Letter { grams } if grams <= 20 => String::from("$1(ふつうの手紙)"), Package::Letter { grams } => format!("$3(大きな手紙、{grams} g)"), Package::Parcel { kg, .. } if kg > 30 => format!("重すぎて発送できません({kg} kg)"), Package::Parcel { kg, express: true } => format!("${}(速達)", 10 + kg * 2), Package::Parcel { kg, express: false } => format!("${}(ふつうの小包)", 5 + kg), }}
fn main() { let packages = vec![ Package::Letter { grams: 15 }, Package::Letter { grams: 80 }, Package::Parcel { kg: 4, express: false }, Package::Parcel { kg: 4, express: true }, Package::Parcel { kg: 35, express: true }, Package::Pickup, ];
for p in packages { println!("{}", shipping_cost(p)); }}実行結果:
$1(ふつうの手紙)$3(大きな手紙、80 g)$9(ふつうの小包)$18(速達)重すぎて発送できません(35 kg)無料(店頭受け取り)おもしろいのは35 kgの速達の小包です。これは2つのアームにマッチしますが、採用されるのは最初の1つだけです。もし速達のアームを「重すぎる」アームより上に置いたら、発送できない小包に平然と$80を請求してしまいます。
最後のアームが .. ではなく express: false と書いている点にも注目してください。必須ではありませんが、こうするとそのアームが何を扱うのかがはっきりします。すべてのケースが網羅されているかは、コンパイラがちゃんとチェックしてくれます。ただし、ガードはそのチェックの対象になりません。だから、ガードのない Package::Letter { grams } のアームが必要なのです。
18. 在庫レポートをきれいにする
Section titled “18. 在庫レポートをきれいにする”倉庫のレポートには、"りんご 12" のように1行に1品目が書かれています。ところが、空行だったり、個数が数値になっていなかったりする壊れた行も混じっています。イテレータのメソッドだけを使って(for ループは使わずに)、パースでき、しかも個数が0より大きい行だけを残してください。それらを 名前: 個数 の形でカンマ区切りで表示し、最後に合計を表示します。
ヒント:.lines() でテキストを行に分けられます。filter_map はクロージャが返した Some の値だけを残すので、絞り込みと変換を一度にできます。.ok() は Result を Option に変えます。イテレータを参照してください。
解答を見る
fn main() { let report = "りんご 12\nバナナ x\n洋なし 0\n\nさくらんぼ 40\nすもも 7";
let in_stock: Vec<(&str, u32)> = report .lines() .filter_map(|line| line.split_once(' ')) .filter_map(|(name, count)| count.parse::<u32>().ok().map(|n| (name, n))) .filter(|&(_, n)| n > 0) .collect();
let list: Vec<String> = in_stock .iter() .map(|(name, n)| format!("{name}: {n}")) .collect(); let total: u32 = in_stock.iter().map(|(_, n)| n).sum();
println!("{}", list.join(", ")); println!("合計個数: {total}");}実行結果:
りんご: 12, さくらんぼ: 40, すもも: 7合計個数: 591行ずつ追ってみましょう。空行にはスペースがないので、split_once は None を返し、最初の filter_map で取り除かれます。"バナナ x" はうまく分割できますが、"x" がパースできないので、2つ目の filter_map で取り除かれます。"洋なし 0" はどちらも通り抜けますが、個数が0なので filter で取り除かれます。
Vec への collect は1回だけにして、それを2回たどっています。1回目はテキストを作るため、2回目は合計を出すためです。イテレータは一度しか使えないので、すべてを2回パースするより、きれいにしたデータを保存しておくほうが簡単です。
次に進むなら
Section titled “次に進むなら”18問すべて解けたなら、お見事です。確かな前進です。🦀
- プログラムをまるごと作ってみましょう。数当てゲーム、単語カウンター、ToDoリストCLIのプロジェクトがおすすめです。
- 就職の準備中なら、Rust面接の質問を読んでみてください。
- もっと練習したいなら、下のリンクにたくさんの問題があります。
さらに詳しく
Section titled “さらに詳しく”- Rustlings(英語):小さな壊れたプログラムを、ターミナルで1つずつ直していく公式の練習セットです。このページの次の一歩にぴったりです。
- ExercismのRustトラック(英語):テスト付きの問題が約100問あり、人間のメンターから解答に無料でフィードバックをもらえます。
- Rust By Example 日本語版:トピックごとの、すぐに動かせる短い例です。ある問題の書き方で行き詰まったときに便利です。
Iteratorトレイト — 標準ライブラリのドキュメント(英語):filter_map、fold、zipなど、イテレータのすべてのメソッドが小さな例付きで載っています。HashSet— 標準ライブラリのドキュメント(英語):union、intersection、differenceを含む、セットのすべてのメソッド。