Progrust Library.

// book chapter

動かして学ぶRustプログラミング問題集

第6章 文字列とベクタ.

基本文法編の最後となるこの章では、文字列とベクタを学びます。
2つの文字列型&strとStringの違いから、文字列の組み立て・切り出し・走査、そして要素数を実行時に増やせるベクタまでを11問で身につけます。

第5章で学んだ配列は要素数がコンパイル時に決まっている必要がありました。この章で扱うStringとベクタは、どちらも実行時に中身を伸ばせる型です。プログラムが扱うデータの多くは実行してみるまで大きさが分からないため、実際のコードで登場する頻度はこちらのほうがずっと高くなります。

進め方は第5章までと同じです。各問題の冒頭に関連する辞書へのリンクを挙げているので、まずはリンク先で必要な知識を確認してから取り組んでください。

01 - 文字列リテラルと&str

文字列リテラルと文字列スライスに関する問題です。
文字列リテラルを型注釈付きで変数に束縛し、その内容と長さを出力してください。型注釈には文字列リテラルの型である&strを書きます。

期待する出力
言語: Rust
バイト数: 4
「Playgroundで開く」をクリックして修正・実行してください
fn 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通りあるので、片方ずつ使ってみてください。

期待する出力
こんにちは
さようなら
「Playgroundで開く」をクリックして修正・実行してください
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!
「Playgroundで開く」をクリックして修正・実行してください
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円
「Playgroundで開く」をクリックして修正・実行してください
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.com
「Playgroundで開く」をクリックして修正・実行してください
fn 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
t
「Playgroundで開く」をクリックして修正・実行してください
fn 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]
「Playgroundで開く」をクリックして修正・実行してください
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つもなかったらどうなるのでしょうか。次の問題で確かめます。

08 - 型注釈で推論を助ける

型推論とベクタ(Vec)に関する問題です。
次のコードはコンパイルエラー(E0282)になります。前の問題では型を書かずにVec::new()と書けたのに、なぜここではエラーになるのでしょうか。エラーメッセージを読み、itemsの宣言を直して修正してください。カートには商品名をStringで入れる想定です。

期待する出力
カートの中身: []
商品数: 0個
「Playgroundで開く」をクリックして修正・実行してください
fn main() {
    // 商品名(String)を入れる空のカートを用意する
    let items = Vec::new();

    println!("カートの中身: {:?}", items);
    println!("商品数: {}個", items.len());
}
Playgroundで開く
解答例と解説
fn main() {
    // 商品名(String)を入れる空のカートを用意する
    let items = Vec::new(); 
    let items: Vec<String> = Vec::new(); 

    println!("カートの中身: {:?}", items);
    println!("商品数: {}個", items.len());
}
Playgroundで開く

エラーメッセージはtype annotations needed for Vec<_>(E0282)で、「型注釈が必要」とそのまま教えてくれます。type must be known at this point(この時点で型が分かっていなければならない)という補足も付きます。

Rustではすべての値の型がコンパイル時に確定していなければなりません。それでも第1章から型注釈をほとんど書かずに済んできたのは、コンパイラが文脈から型を決めてくれる型推論という仕組みのおかげです。

Vec<T>のTは「要素の型」を表す穴で、ここが埋まって初めてVec<String>やVec<i32>という具体的な型になります。問題07ではVec::new()の時点で穴が空いていても、後の行のnumbers.push(10)が手がかりになってVec<i32>だと決まりました。推論の材料はletの右辺だけでなく、同じ関数の中の後の行の使われ方も含むわけです。

fn main() {
    let mut numbers = Vec::new(); // この時点では要素の型は未確定
    numbers.push(10); // 後の行から Vec<i32> だと決まる

    println!("{:?}", numbers);
}
Playgroundで開く

ところが今回のコードにはpushがありません。{:?}での出力もlen()も、要素が何の型かを絞り込む手がかりにはならないため、Tは最後まで埋まらないままです。コンパイラは「決めきれないので教えてほしい」とエラーを出します。

書き方 手がかり 結果
let mut v = Vec::new(); v.push(10); 後のpush Vec<i32>に決まる
let v = Vec::new();(pushなし) なし E0282
let v: Vec<String> = Vec::new(); 型注釈 Vec<String>に決まる

型注釈は「毎回書くもの」ではなく、手がかりがないときにだけ書き足すものだと考えてください。空のベクタを先に用意して後から詰めていくコードでは、この問題のように注釈が必要になる場面がときどきあります。

09 - ベクタの要素にアクセスする

ベクタ(Vec)に関する問題です。
ベクタの最初の要素・要素数・最後の要素を出力してください。最後の要素のインデックスは決め打ちにせず、要素数から求めてください。

期待する出力
最初の商品: 120円
商品数: 4個
最後の商品: 300円
「Playgroundで開く」をクリックして修正・実行してください
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と書いておけば、要素数がいくつになっても常に最後の要素を指します。

10 - for式でベクタを走査する

ベクタ(Vec)とfor式に関する問題です。
ベクタの全要素を出力しながら、合計点を計算してください。

期待する出力
80点
95点
70点
合計: 245点
「Playgroundで開く」をクリックして修正・実行してください
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章の応用問題と同じ考え方です。

11 - 応用: 成績集計

基本文法編の総復習として、ベクタ(Vec)・文字列型・関数・for式・キャストを組み合わせた問題です。
点数のリストを受け取り、合計: 245点 / 平均: 81.7点という形式のStringを返す関数summarizeを実装して、テストに合格させてください。平均は小数第1位まで表示します({:.1}で桁数を指定できます)。

「Playgroundで開く」をクリックしてTESTを実行してください
// 点数のスライス&[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とそのまま書けます(問題10の&scoresは、&の付いていないベクタを借りるために必要でした)。

平均を求める
totalはi32、scores.len()は要素数を表す整数で、どちらも整数のままでは割り算の結果が切り捨てられてしまいます。第2章のキャストで両方をf64に揃えてから割ります。245 ÷ 3 = 81.666… なので、切り捨ててしまうと81になり、テストに失敗します。

文字列を組み立てる
最後はformat!で仕上げます。{:.1}と書くと小数第1位まで(四捨五入して)表示されるので、81.666… は81.7になります。{}のままだと81.66666666666667と表示されてテストに失敗します。

これで第6章、そして基本文法編は終わりです。ここまでで、変数・型・制御フロー・関数・複合型と、Rustのプログラムを読み書きする土台がそろいました。

一方で、この章の問題10やStringの+演算子のように、「なぜか後から使えなくなる」という説明を保留にしたままの話題がいくつか残っています。その答えがRust最大の特徴である所有権です。次の章から、いよいよそこに踏み込んでいきます。