コース目次 / 第3章

成果物 — 買い物メモ

打ち込んだものをためて合計を出すCLIを、Result のエラー処理つきで作ります。

第3章 / 全5章目安 約60分この章のゴール: 名前と値段を打ち込むと、一覧と合計が出る
お使いのエディタ
ファイル操作が切り替わります

いよいよ道具を作ります。打ち込んだものをためて、合計を出す買い物メモです。

今日のゴール

こうなります
> りんご 180
りんご を入れました。
> パン 250
パン を入れました。
> list
1. りんご 180円
2. パン 250円
合計 430円
> quit
またどうぞ。

1. ミニ解説: 今日使う4つ

書き方 何をするか
struct Mono { ... } 形に名前を付ける。「名前と値段を持つもの」を1つの型にする
Vec<Mono> 並べてためる入れ物。あとから増やせる
match 値 { ... } 場合分け。全部の場合を書くまで通らない
.parse::<u32>() 文字を数に変える。うまくいったかどうかごと返ってくる(これが Result)

Result がこの章の主役です。「文字を数に変える」は失敗しうる処理です。多くの言語では、失敗したときに例外が飛ぶか、こっそり0が返ります。Rustは「成功(Ok)か失敗(Err)か」を値そのものに含めて返すので、受け取る側は両方を書かないとコンパイルが通りません。
試食章の match の網羅チェックが、ここで効いてきます。

2. 書く

src/main.rs を、まるごとこう書き換えます。少し長いですが、上から順に読めるはずです。

src/main.rs
use std::io::{self, Write};

struct Mono {
    namae: String,
    nedan: u32,
}

fn main() {
    let mut memo: Vec<Mono> = Vec::new();

    println!("買い物メモ");
    println!("「名前 値段」で追加、list で一覧、quit で終わり。");

    loop {
        print!("> ");
        io::stdout().flush().unwrap();

        let mut gyou = String::new();
        if io::stdin().read_line(&mut gyou).unwrap() == 0 {
            break;
        }

        match gyou.trim() {
            "" => continue,
            "quit" => break,
            "list" => ichiran(&memo),
            hoka => tsuika(&mut memo, hoka),
        }
    }

    println!("またどうぞ。");
}

fn tsuika(memo: &mut Vec<Mono>, gyou: &str) {
    let bubun: Vec<&str> = gyou.split_whitespace().collect();

    if bubun.len() != 2 {
        println!("「名前 値段」のように、2つに分けて書いてください。");
        return;
    }

    // parse は「うまくいったかどうか」ごと返してくる。両方の場合を書かないと通らない。
    match bubun[1].parse::<u32>() {
        Ok(nedan) => {
            memo.push(Mono {
                namae: bubun[0].to_string(),
                nedan,
            });
            println!("{} を入れました。", bubun[0]);
        }
        Err(_) => {
            println!("値段は数字で書いてください。「{}」は数字に見えません。", bubun[1]);
        }
    }
}

fn ichiran(memo: &[Mono]) {
    if memo.is_empty() {
        println!("まだ何も入っていません。");
        return;
    }

    let mut goukei = 0;

    for (i, mono) in memo.iter().enumerate() {
        println!("{}. {} {}円", i + 1, mono.namae, mono.nedan);
        goukei += mono.nedan;
    }

    println!("合計 {}円", goukei);
}

読みどころが3つあります

(1) &mut と &

tsuika は &mut Vec<Mono>(書き換えるために借りる)、ichiran は &[Mono](見るだけ借りる)を受け取っています。試食章でやった「借りる」が、ここで実用になっています。書き換える側だけ mut を付けるので、読んだ人は「この関数は中身を変える」と一目で分かります。

(2) match gyou.trim() の hoka

""・"quit"・"list" 以外の全部を hoka という名前で受け取っています。match は「それ以外」を書かないと通りません。書き忘れられない仕組みです。

(3) Ok と Err

parse の結果を match しています。成功した中身を nedan として取り出せるのは Ok の枝の中だけです。失敗しているかもしれない値を、うっかりそのまま使うことができません。

3. 動かす

VS Codeのターミナルターミナル
cargo run

行頭の $ は「ここからコマンド」という目印です。$ は打たないでください。

いくつか打ってみてください。

やってみる
> りんご 180
> パン 250
> list
> quit

こわしてみる

2つ、わざと変なことをしてください。どちらも落ちません。

(1) 値段に文字を打つ

打ってみる
> コーヒー あああ
こう出る
値段は数字で書いてください。「あああ」は数字に見えません。

プログラムは止まりません。 Err の枝がそれを引き受けています。

(2) 名前だけ打つ

打ってみる
> りんご
こう出る
「名前 値段」のように、2つに分けて書いてください。

「落ちない」は、書いた人が偉いのではありません。Rustが「失敗しうる場所では、失敗の場合も書け」と強制した結果です。この強制が、Rustが「安全」と言われる理由の半分です(もう半分が所有権です)。

ハマったら

「mismatched types」と出る

型が合っていません。エラーの中に expected と found が出ているので、その2つを見比べてください。u32 と i32、String と &str の取り違えがほとんどです。

「cannot borrow as mutable」と出る

tsuika(&mut memo, hoka) の &mut が抜けているか、let mut memo の mut が抜けています。エラーの help: に、どちらを直すか書いてあります。

「non-exhaustive patterns」と出る

match の枝が足りません。hoka => ... の行(それ以外の場合)を書き忘れていないか確かめてください。

ZIPで戻したい

このページの下のZIPは、この章を終えた状態のフォルダです。解凍して、その中で作業を続けられます。自分で書き換えた部分は消えてしまうので、先に自分のファイルを別の場所へ待避してから使ってください。

こうなっていればOK

うまくいかないときは、この章の完成ファイル一式(ZIP)をダウンロードして続きから進めても大丈夫です。

卒業まであと1章です。

この章はまだ完了していません。