// book chapter
動かして学ぶRustプログラミング問題集第6章 文字列とベクタ.
基本文法編の最後となるこの章では、文字列とベクタを学びます。
2つの文字列型&strとStringの違いから、文字列の組み立て・切り出し・走査、そして要素数を実行時に増やせるベクタまでを10問で身につけます。
第5章で学んだ配列は要素数がコンパイル時に決まっている必要がありました。この章で扱うStringとベクタは、どちらも実行時に中身を伸ばせる型です。プログラムが扱うデータの多くは実行してみるまで大きさが分からないため、実際のコードで登場する頻度はこちらのほうがずっと高くなります。
進め方は第5章までと同じです。各問題の冒頭に関連する辞書へのリンクを挙げているので、まずはリンク先で必要な知識を確認してから取り組んでください。
01 - 文字列リテラルと&str
文字列リテラルと文字列スライスに関する問題です。
文字列リテラルを型注釈付きで変数に束縛し、その内容と長さを出力してください。型注釈には文字列リテラルの型である&strを書きます。
言語: Rust
バイト数: 4fn main() {
// 文字列リテラル"Rust"を、&strの型注釈付きで変数languageに束縛せよ
println!("言語: {}", language);
println!("バイト数: {}", language.len());
}Playgroundで開く解答例と解説
fn main() {
// 文字列リテラル"Rust"を、&strの型注釈付きで変数languageに束縛せよ
let language: &str = "Rust";
println!("言語: {}", language);
println!("バイト数: {}", language.len());
}Playgroundで開く第1章から使ってきた"Rust"のような文字列リテラルの型は&strで、文字列スライスと読みます。第5章で配列の一部を借りるスライス&[i32]を作りましたが、&strもそれと同じ考え方の型です。文字列データそのものはプログラムの中に埋め込まれていて、&strはその位置と長さを指しているだけ、という関係になっています。
len()が返すのは文字数ではなくバイト数です。"Rust"は半角英字4文字なので4バイトですが、日本語だと結果が変わります。
fn main() {
println!("{}", "Rust".len()); // 4
println!("{}", "さび".len()); // 6(ひらがなは1文字3バイト)
}Playgroundで開くRustの文字列はUTF-8という方式で表現されていて、この方式では文字によってバイト数が変わります(半角英数字は1バイト、ひらがなや漢字は3バイトなど)。「何文字か」を数えたい場合は、問題06で扱うchars()を使います。
02 - String型
文字列型に関する問題です。
2つの文字列リテラルからString型の値をそれぞれ作り、出力してください。Stringを作る方法は2通りあるので、片方ずつ使ってみてください。
こんにちは
さようならfn main() {
// String::fromを使って"こんにちは"からStringを作り、greetingに束縛せよ
// to_stringを使って"さようなら"からStringを作り、farewellに束縛せよ
println!("{}", greeting);
println!("{}", farewell);
}Playgroundで開く解答例と解説
fn main() {
// String::fromを使って"こんにちは"からStringを作り、greetingに束縛せよ
let greeting = String::from("こんにちは");
// to_stringを使って"さようなら"からStringを作り、farewellに束縛せよ
let farewell = "さようなら".to_string();
println!("{}", greeting);
println!("{}", farewell);
}Playgroundで開くString::from("...")と"...".to_string()は、どちらも文字列リテラルからString型の値を作ります。結果は同じなので、読みやすいほうを選んで構いません。
Stringと&strの違いは内容を変更できるかどうかです。
| 型 | 内容の変更 | 主な用途 |
|---|---|---|
&str |
できない | 決まった文字列を読む・関数に渡す |
String |
できる | 実行時に組み立てる文字列を持つ |
&strが指しているのはプログラムに埋め込まれた固定のデータなので、後から書き換えることはできません。一方Stringは自分専用の領域に文字列をコピーして持つため、後から文字を足したり削ったりできます。次の問題で実際に伸ばしてみます。
03 - 文字列に追記する
文字列型と文字型に関する問題です。
Stringに文字列と文字を追記して、「Hello, world!」を組み立ててください。追記にはpush_strとpushの2つのメソッドを使います。
Hello, world!fn main() {
let mut message = String::from("Hello");
// push_strでmessageに", world"を追記せよ
// pushでmessageに'!'を追記せよ
println!("{}", message);
}Playgroundで開く解答例と解説
fn main() {
let mut message = String::from("Hello");
// push_strでmessageに", world"を追記せよ
message.push_str(", world");
// pushでmessageに'!'を追記せよ
message.push('!');
println!("{}", message);
}Playgroundで開くpush_strは文字列(&str)を、pushは1文字(char)を末尾に追加するメソッドです。渡す値の型が違う点に注意してください。push('!')はシングルクォート、push_str(", world")はダブルクォートです。第2章で学んだ「クォートの種類で型が変わる」という話がここで効いてきます。
追記は変数の内容を変更する操作なので、let mut message = ...とmutを付けておく必要があります。付け忘れると「cannot borrow message as mutable」というコンパイルエラー(E0596)になります。
Stringは追加した内容が入りきらなくなると、自動でより大きな領域を確保し直します。そのため「あと何文字入るか」を気にせずに追記していけます。
04 - format!で組み立てる
文字列型とコンソール出力に関する問題です。
複数の値から1つのStringを組み立ててください。println!のように書けて、画面に出力する代わりにStringを返すのがformat!マクロです。
りんご 150円 × 3個
合計: 450円fn main() {
let item = "りんご";
let price = 150;
let count = 3;
// format!を使って「りんご 150円 × 3個」というStringを作り、labelに束縛せよ
println!("{}", label);
println!("合計: {}円", price * count);
}Playgroundで開く解答例と解説
fn main() {
let item = "りんご";
let price = 150;
let count = 3;
// format!を使って「りんご 150円 × 3個」というStringを作り、labelに束縛せよ
let label = format!("{} {}円 × {}個", item, price, count);
println!("{}", label);
println!("合計: {}円", price * count);
}Playgroundで開くformat!はprintln!とまったく同じ書き方でプレースホルダに値を埋め込みますが、画面には出力せず、組み立てた結果をStringとして返すマクロです。第1章から使ってきたprintln!の知識をそのまま使えます。
値として返ってくるので、変数に束縛したり、関数の戻り値にしたり、後からさらに追記したりできます。「文字列を組み立てたいが、すぐには出力しない」という場面ではこれを使います。
push_strを繰り返しても同じものは作れますが、format!のほうが完成形が一目で分かります。
fn main() {
let item = "りんご";
let price = 150;
let count = 3;
// format!を使わずに書くと手間がかかる
let mut label = String::new();
label.push_str(item);
label.push_str(" ");
label.push_str(&price.to_string());
label.push_str("円 × ");
label.push_str(&count.to_string());
label.push_str("個");
println!("{}", label);
}Playgroundで開く05 - 文字列スライスを切り出す
文字列スライスとスライスに関する問題です。
メールアドレスから、ユーザー名の部分(@より前)とドメインの部分(@より後ろ)を切り出して出力してください。
ユーザー名: hanako
ドメイン: example.comfn main() {
let email = "[email protected]";
// emailの先頭6文字(インデックス0から5)を切り出してuserに束縛せよ
// emailのインデックス7から末尾までを切り出してdomainに束縛せよ
println!("ユーザー名: {}", user);
println!("ドメイン: {}", domain);
}Playgroundで開く解答例と解説
fn main() {
let email = "[email protected]";
// emailの先頭6文字(インデックス0から5)を切り出してuserに束縛せよ
let user = &email[0..6];
// emailのインデックス7から末尾までを切り出してdomainに束縛せよ
let domain = &email[7..];
println!("ユーザー名: {}", user);
println!("ドメイン: {}", domain);
}Playgroundで開く文字列の一部を切り出す書き方は、第5章の配列と同じ&値[範囲]です。切り出した結果の型も&str(文字列スライス)になります。
&email[7..]のように終端を省略すると「そこから末尾まで」という意味になります。同様に&email[..6]と開始を省略すれば「先頭からそこまで」です。範囲式のこの省略記法は配列のスライスでも使えます。
| 書き方 | 意味 |
|---|---|
&s[0..6] |
インデックス0から6の手前まで |
&s[7..] |
インデックス7から末尾まで |
&s[..6] |
先頭から6の手前まで |
&s[..] |
全体 |
06 - 1文字ずつ処理する
文字列型・文字型・for式に関する問題です。
文字列を1文字ずつ取り出して、1行に1文字ずつ出力してください。
R
u
s
tfn main() {
let word = "Rust";
// chars()とfor式でwordを1文字ずつ取り出して出力せよ
}Playgroundで開く解答例と解説
fn main() {
let word = "Rust";
// chars()とfor式でwordを1文字ずつ取り出して出力せよ
for c in word.chars() {
println!("{}", c);
}
}Playgroundで開くchars()は文字列をchar(1文字)の並びに分解するメソッドです。for式と組み合わせると、先頭から1文字ずつ取り出して処理できます。取り出されるcの型は&strではなく文字型のcharです。
問題01で見たとおり、len()が数えるのはバイト数でした。文字数を数えたいときはchars()と組み合わせてword.chars().count()と書きます。
fn main() {
let word = "さび";
println!("バイト数: {}", word.len()); // 6
println!("文字数: {}", word.chars().count()); // 2
}Playgroundで開く日本語を含む文字列でもchars()なら1文字ずつ正しく取り出せます。問題05の範囲指定と違い、文字の途中で切れてパニックする心配もありません。
07 - ベクタを作る
ベクタ(Vec)に関する問題です。
2通りの方法でベクタを作ってください。1つはvec!マクロで最初から要素を並べる方法、もう1つは空のベクタを作ってからpushで追加する方法です。
[120, 250, 80]
[10, 20]fn main() {
// vec!マクロで120, 250, 80を要素に持つベクタpricesを作れ
// Vec::newで空のベクタを作りnumbersに束縛し、pushで10と20を順に追加せよ
println!("{:?}", prices);
println!("{:?}", numbers);
}Playgroundで開く解答例と解説
fn main() {
// vec!マクロで120, 250, 80を要素に持つベクタpricesを作れ
let prices = vec![120, 250, 80];
// Vec::newで空のベクタを作りnumbersに束縛し、pushで10と20を順に追加せよ
let mut numbers = Vec::new();
numbers.push(10);
numbers.push(20);
println!("{:?}", prices);
println!("{:?}", numbers);
}Playgroundで開くベクタVec<T>は、同じ型の値を可変長で並べるための型です。第5章の配列とよく似ていますが、実行時に要素を追加・削除できる点が決定的に違います。
配列 [i32; 3] |
ベクタ Vec<i32> |
|
|---|---|---|
| 要素数 | コンパイル時に固定 | 実行時に増減できる |
| 要素数が型の一部 | ○ | ✗ |
作り方は主に2通り使い分けます。最初から中身が決まっているならvec![120, 250, 80]、これから貯めていくならVec::new()で空のベクタを作ってpushで追加します。pushは内容を変更する操作なので、let mutが必要です。
Vec::new()には要素の型を書いていませんが、後のnumbers.push(10)から「これは整数のベクタだ」とコンパイラが推論してくれます。もしpushが1つもないと型を決められず、let numbers: Vec<i32> = Vec::new();のような型注釈が必要になります。
08 - ベクタの要素にアクセスする
ベクタ(Vec)に関する問題です。
ベクタの最初の要素・要素数・最後の要素を出力してください。最後の要素のインデックスは決め打ちにせず、要素数から求めてください。
最初の商品: 120円
商品数: 4個
最後の商品: 300円fn main() {
let mut prices = vec![120, 250, 80];
prices.push(300);
// 最初の要素を「最初の商品: 120円」の形式で出力せよ
// 要素数を「商品数: 4個」の形式で出力せよ
// 最後の要素を「最後の商品: 300円」の形式で出力せよ
}Playgroundで開く解答例と解説
fn main() {
let mut prices = vec![120, 250, 80];
prices.push(300);
// 最初の要素を「最初の商品: 120円」の形式で出力せよ
println!("最初の商品: {}円", prices[0]);
// 要素数を「商品数: 4個」の形式で出力せよ
println!("商品数: {}個", prices.len());
// 最後の要素を「最後の商品: 300円」の形式で出力せよ
println!("最後の商品: {}円", prices[prices.len() - 1]);
}Playgroundで開く要素へのアクセスprices[0]も、要素数を返すlen()も、書き方は配列とまったく同じです。第5章で学んだことがそのまま使えます。
最後の要素はprices[3]と書いても今回は正しく動きますが、pushで要素が増減すると途端に間違いになります。prices.len() - 1と書いておけば、要素数がいくつになっても常に最後の要素を指します。
09 - for式でベクタを走査する
ベクタ(Vec)とfor式に関する問題です。
ベクタの全要素を出力しながら、合計点を計算してください。
80点
95点
70点
合計: 245点fn main() {
let scores = vec![80, 95, 70];
let mut total = 0;
// for式でscoresを走査し、各要素を「80点」の形式で出力しつつtotalに足し込め
println!("合計: {}点", total);
}Playgroundで開く解答例と解説
fn main() {
let scores = vec![80, 95, 70];
let mut total = 0;
// for式でscoresを走査し、各要素を「80点」の形式で出力しつつtotalに足し込め
for score in &scores {
println!("{}点", score);
total += score;
}
println!("合計: {}点", total);
}Playgroundで開く走査の書き方は配列のときと同じfor式ですが、ベクタの前に&を付けている点に注目してください。
for score in scoresと&なしで書いてもこのコードは動きます。ただしその場合、走査した後でscoresが使えなくなります。for式にベクタを渡すと、ベクタそのものがfor式に引き渡されてしまうためです。&を付けると「中身を借りるだけ」という意味になり、走査した後もscoresをそのまま使い続けられます。
fn main() {
let scores = vec![80, 95, 70];
for score in &scores {
println!("{}点", score);
}
println!("要素数: {}", scores.len()); // &を付けて走査したので、ここで使える
}Playgroundで開く合計を貯めるtotalをループの外で宣言するのは、第5章の応用問題と同じ考え方です。
10 - 応用: 成績集計
基本文法編の総復習として、ベクタ(Vec)・文字列型・関数・for式・キャストを組み合わせた問題です。
点数のリストを受け取り、合計: 245点 / 平均: 81.7点という形式のStringを返す関数summarizeを実装して、テストに合格させてください。平均は小数第1位まで表示します({:.1}で桁数を指定できます)。
// 点数のスライス&[i32]を受け取り、「合計: 245点 / 平均: 81.7点」の形式のStringを返す
// 関数summarizeを定義せよ
#[test]
fn test_summarize() {
let scores = vec![80, 95, 70];
assert_eq!(summarize(&scores), "合計: 245点 / 平均: 81.7点");
}
#[test]
fn test_summarize_pushed() {
let mut scores = Vec::new();
scores.push(100);
scores.push(90);
assert_eq!(summarize(&scores), "合計: 190点 / 平均: 95.0点");
}
#[test]
fn test_summarize_single() {
assert_eq!(summarize(&[70]), "合計: 70点 / 平均: 70.0点");
}Playgroundで開く解答例と解説
// 点数のスライス&[i32]を受け取り、「合計: 245点 / 平均: 81.7点」の形式のStringを返す
// 関数summarizeを定義せよ
fn summarize(scores: &[i32]) -> String {
let mut total = 0;
for score in scores {
total += score;
}
let average = total as f64 / scores.len() as f64;
format!("合計: {}点 / 平均: {:.1}点", total, average)
}
#[test]
fn test_summarize() {
let scores = vec![80, 95, 70];
assert_eq!(summarize(&scores), "合計: 245点 / 平均: 81.7点");
}
#[test]
fn test_summarize_pushed() {
let mut scores = Vec::new();
scores.push(100);
scores.push(90);
assert_eq!(summarize(&scores), "合計: 190点 / 平均: 95.0点");
}
#[test]
fn test_summarize_single() {
assert_eq!(summarize(&[70]), "合計: 70点 / 平均: 70.0点");
}Playgroundで開く基本文法編の総まとめとして、これまでの章の要素が一通り登場します。ポイントを順に見ていきましょう。
引数は&[i32]で受ける
第5章と同じくスライスで受け取っています。テストでは&scores(ベクタへの参照)と&[70](配列への参照)の両方を渡していますが、どちらも&[i32]として扱えるため、同じ関数で受けられます。ベクタでも配列でも受け取れるので、列を読むだけの関数の引数は&[T]にするのが定番です。
合計を求める
scoresはすでに&の付いたスライスなので、for score in scoresとそのまま書けます(問題09の&scoresは、&の付いていないベクタを借りるために必要でした)。
平均を求める
totalはi32、scores.len()は要素数を表す整数で、どちらも整数のままでは割り算の結果が切り捨てられてしまいます。第2章のキャストで両方をf64に揃えてから割ります。245 ÷ 3 = 81.666… なので、切り捨ててしまうと81になり、テストに失敗します。
文字列を組み立てる
最後はformat!で仕上げます。{:.1}と書くと小数第1位まで(四捨五入して)表示されるので、81.666… は81.7になります。{}のままだと81.66666666666667と表示されてテストに失敗します。
これで第6章、そして基本文法編は終わりです。ここまでで、変数・型・制御フロー・関数・複合型と、Rustのプログラムを読み書きする土台がそろいました。
一方で、この章の問題09やStringの+演算子のように、「なぜか後から使えなくなる」という説明を保留にしたままの話題がいくつか残っています。その答えがRust最大の特徴である所有権です。次の章から、いよいよそこに踏み込んでいきます。