Progrust Library.

// book chapter

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

第10章 メソッドと関連関数.

第9章では、自分で型を定義してデータをまとめられるようになりました。
この章では、その型に振る舞いを与える方法——implブロックによるメソッドと関連関数——を8問で身につけます。

ここまでString::from("...")vec.push(3)のように、型に紐づいた関数をずっと使ってきました。String::fromのように型名から呼ぶものを関連関数vec.pushのように値から呼ぶものをメソッドと呼びます。この章を終えれば、これらを自分の型にも定義できるようになります。

メソッドはただの関数と違い、第一引数にselfを取ります。このself&self&mut selfselfのどれで受け取るかは、第8章で学んだ「読むだけか、書き換えるか、所有権ごと消費するか」という判断そのものです。所有権と借用の知識がここで効いてきます。

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

01 - メソッドを定義する

implブロックメソッドに関する問題です。
在庫の合計金額(price × stock)を返すメソッドtotal_priceを定義してください。

期待する出力
コーヒー豆の在庫金額: 3600円
「Playgroundで開く」をクリックして修正・実行してください
struct Product {
    name: String,
    price: u32,
    stock: u32,
}

impl Product {
    // 在庫の合計金額(price × stock)を返すメソッドtotal_priceを定義せよ

}

fn main() {
    let product = Product {
        name: String::from("コーヒー豆"),
        price: 1200,
        stock: 3,
    };

    println!("{}の在庫金額: {}円", product.name, product.total_price());
}
Playgroundで開く
解答例と解説
struct Product {
    name: String,
    price: u32,
    stock: u32,
}

impl Product {
    // 在庫の合計金額(price × stock)を返すメソッドtotal_priceを定義せよ
    fn total_price(&self) -> u32 { 
        self.price * self.stock 
    } 
}

fn main() {
    let product = Product {
        name: String::from("コーヒー豆"),
        price: 1200,
        stock: 3,
    };

    println!("{}の在庫金額: {}円", product.name, product.total_price());
}
Playgroundで開く

impl 型名 { ... }という形のブロックをimplブロックと呼びます。この中に書いた関数はその型に紐づき、第一引数にselfを取るものがメソッドになります。

普通の関数との違いは2つだけです。

普通の関数 メソッド
書く場所 どこでも implブロックの中
第一引数 自由 self(またはその参照)
呼び出し方 total_price(&product) product.total_price()

&selfは「このメソッドを呼んだインスタンスへの共有参照」です。中ではself.priceのようにフィールドへアクセスできます。第9章の問題07で書いたfn print_product(product: &Product)とやっていることは同じで、引数名がproductからselfに変わり、implブロックの中に移っただけです。

では、なぜ関数ではなくメソッドにするのでしょうか。

  • その型に関する処理が1箇所にまとまるProductに何ができるのかはimpl Productを見れば分かります
  • 呼び出しが読みやすいtotal_price(&product)よりproduct.total_price()のほうが「商品の在庫金額」と読めます
  • &を書かなくてよい。メソッド呼び出しでは参照が自動で補われます(問題04で詳しく扱います)

02 - selfを忘れると

メソッドimplブロックに関する問題です。
次のコードはコンパイルエラー(E0424)になります。with_taxの定義を1箇所だけ直してください。

期待する出力
コーヒー豆: 税込み1320円
「Playgroundで開く」をクリックして修正・実行してください
struct Product {
    name: String,
    price: u32,
}

impl Product {
    fn with_tax() -> u32 {
        self.price * 110 / 100
    }
}

fn main() {
    let product = Product {
        name: String::from("コーヒー豆"),
        price: 1200,
    };

    println!("{}: 税込み{}円", product.name, product.with_tax());
}
Playgroundで開く
解答例と解説
struct Product {
    name: String,
    price: u32,
}

impl Product {
    fn with_tax() -> u32 { 
    fn with_tax(&self) -> u32 { // &selfを追加した
        self.price * 110 / 100
    }
}

fn main() {
    let product = Product {
        name: String::from("コーヒー豆"),
        price: 1200,
    };

    println!("{}: 税込み{}円", product.name, product.with_tax());
}
Playgroundで開く

エラーメッセージはexpected value, found module 'self'(E0424)で、selfを書いた箇所には'self' value is a keyword only available in methods with a 'self' parameter——「selfself引数を持つメソッドの中でしか使えないキーワードだ」と説明が付きます。ヒントにはadd a 'self' receiver parameter to make the associated 'fn' a methodと、修正方法まで書かれています。

implブロックの中に書いただけではメソッドになりません。第一引数にselfを書いて初めてメソッドになりますselfを取らない関数もimplブロックには書けますが、それは関連関数と呼ばれる別のもので、product.with_tax()というドット記法では呼べません(問題06で扱います)。

selfは引数名であると同時に、Rustのキーワードでもあります。そのため、関数の引数リストにselfがなければ「そんな値はない」ではなく「メソッドでないと使えないキーワードだ」という専用のエラーになります。

implブロックの中に書ける2種類を整理しておきます。

種類 第一引数 呼び出し方
メソッド selfあり product.with_tax()
関連関数 selfなし Product::with_tax()

03 - &mut selfで書き換える

メソッド参照に関する問題です。
在庫をcount個増やすメソッドrestockを定義してください。

期待する出力
コーヒー豆の在庫: 8個
「Playgroundで開く」をクリックして修正・実行してください
struct Product {
    name: String,
    stock: u32,
}

impl Product {
    // 在庫をcount個増やすメソッドrestockを定義せよ

}

fn main() {
    let mut product = Product {
        name: String::from("コーヒー豆"),
        stock: 3,
    };

    product.restock(5);

    println!("{}の在庫: {}個", product.name, product.stock);
}
Playgroundで開く
解答例と解説
struct Product {
    name: String,
    stock: u32,
}

impl Product {
    // 在庫をcount個増やすメソッドrestockを定義せよ
    fn restock(&mut self, count: u32) { 
        self.stock += count; 
    } 
}

fn main() {
    let mut product = Product {
        name: String::from("コーヒー豆"),
        stock: 3,
    };

    product.restock(5);

    println!("{}の在庫: {}個", product.name, product.stock);
}
Playgroundで開く

フィールドを書き換えるメソッドは&mut selfで受け取ります。第8章で学んだ排他参照がそのまま出てきただけで、&selfのままではself.stock += countが「共有参照から書き換えようとしている」ことになりエラーです。

selfの後ろには、普通の関数と同じように追加の引数を並べられます。selfは必ず第一引数でなければならない、という点だけが決まりです。

selfの3つの形式を整理します。この章の問題05で3つめを扱います。

記法 受け取り方 用途
&self 共有参照 フィールドを読むだけ(最も多い)
&mut self 排他参照 フィールドを書き換える
self 所有権ごと インスタンスを消費・変換する

迷ったらまず&selfで書き、書き換えが必要になった時点で&mut selfにする、という順で考えると自然に選べます。

04 - 自動参照とmut

自動参照・自動参照外しメソッド借用に関する問題です。
次のコードはコンパイルエラー(E0596)になります。mainの中の1行に1単語だけ足して修正してください。

期待する出力
コーヒー豆の在庫: 10個
「Playgroundで開く」をクリックして修正・実行してください
struct Product {
    name: String,
    stock: u32,
}

impl Product {
    fn restock(&mut self, count: u32) {
        self.stock += count;
    }
}

fn main() {
    let product = Product {
        name: String::from("コーヒー豆"),
        stock: 3,
    };

    product.restock(7);

    println!("{}の在庫: {}個", product.name, product.stock);
}
Playgroundで開く
解答例と解説
struct Product {
    name: String,
    stock: u32,
}

impl Product {
    fn restock(&mut self, count: u32) {
        self.stock += count;
    }
}

fn main() {
    let product = Product { 
    let mut product = Product { // mutを追加した
        name: String::from("コーヒー豆"),
        stock: 3,
    };

    product.restock(7);

    println!("{}の在庫: {}個", product.name, product.stock);
}
Playgroundで開く

エラーメッセージはcannot borrow 'product' as mutable, as it is not declared as mutable(E0596)です。第8章の問題05とまったく同じ文面ですが、あちらでは&mut balanceと自分で書いていたのに対し、今回のコードには&mutがどこにも見当たりません。

product.restock(7)と書いたとき、コンパイラはrestockの第一引数が&mut selfであることを見て、レシーバ(ドットの左側)に&mutを自動で補います

この2行はまったく同じ意味
product.restock(7);
Product::restock(&mut product, 7);

この仕組みを自動参照・自動参照外しと呼びます。メソッドが&selfを取るなら&が、&mut selfを取るなら&mutが自動的に付き、レシーバが参照であれば逆に*が補われます。おかげで、どのメソッドを呼ぶときも同じドット記法で書けます。

便利な反面、借用が起きていることが見た目に現れませんproduct.restock(7)はただの呼び出しに見えて、実際にはproductを排他借用しています。だからこそ所有者の宣言にmutが必要になります。

第8章の問題03で挙げた「3箇所のmut」を、メソッド呼び出しの場合に置き換えると次のようになります。

場所 関数呼び出しの場合 メソッド呼び出しの場合
所有者の宣言 let mut product let mut product(必要)
参照を作る側 &mut product 自動で補われる(不要)
受け取る側 product: &mut Product &mut self

自分で書く&mutが消えただけで、借用そのものは起きています。

05 - selfで所有権を消費する

メソッドムーブに関する問題です。
次のコードはコンパイルエラー(E0382)になります。行の順番を入れ替えるだけで、下の出力になるよう修正してください。

期待する出力
注文内容: コーヒー ×2
レシート: コーヒー ×2
「Playgroundで開く」をクリックして修正・実行してください
struct Order {
    item: String,
    quantity: u32,
}

impl Order {
    fn into_receipt(self) -> String {
        format!("{} ×{}", self.item, self.quantity)
    }
}

fn main() {
    let order = Order {
        item: String::from("コーヒー"),
        quantity: 2,
    };

    println!("レシート: {}", order.into_receipt());
    println!("注文内容: {} ×{}", order.item, order.quantity);
}
Playgroundで開く
解答例と解説
struct Order {
    item: String,
    quantity: u32,
}

impl Order {
    fn into_receipt(self) -> String {
        format!("{} ×{}", self.item, self.quantity)
    }
}

fn main() {
    let order = Order {
        item: String::from("コーヒー"),
        quantity: 2,
    };

    println!("レシート: {}", order.into_receipt()); 
    println!("注文内容: {} ×{}", order.item, order.quantity); // 先に読んでおく
    println!("レシート: {}", order.into_receipt()); // ここでorderを消費する
}
Playgroundで開く

エラーメッセージはborrow of moved value: 'order'(E0382)です。第7章で何度も見たムーブのエラーが、メソッド呼び出しで起きています。

into_receiptの第一引数は&selfでも&mut selfでもなく、参照の付かないselfです。これはインスタンスの所有権ごと受け取るという宣言で、呼び出した時点でorderの所有権はメソッドへムーブします。メソッドを抜けるとselfは破棄されるので、呼び出し元のorderは二度と使えません。

つまりorder.into_receipt()は、第7章で見たconsume(order)という関数呼び出しとまったく同じことをしています。ドット記法で書かれていると見落としやすいので、selfを取るメソッドは要注意です。

記法 呼び出し後のインスタンス
&self そのまま使える
&mut self そのまま使える(中身は変わっているかもしれない)
self もう使えない

修正は、orderを読む行を消費する前に持ってくることです。into_receiptを呼んだ後はorderが存在しないので、それより後にorder.itemを読むことはできません。

selfを取るメソッドは、インスタンスを別の形に変換して、元の形はもう使わないという場面で使います。標準ライブラリでもStringからVec<u8>を作るinto_bytesなど、into_で始まる名前のメソッドがこの形になっているのが慣習です。

06 - 関連関数でコンストラクタ

関連関数implブロックに関する問題です。
商品名と価格を受け取り、在庫0のProductを返す関連関数newを定義し、mainから呼び出してください。

期待する出力
コーヒー豆: 1200円 / 在庫0個
「Playgroundで開く」をクリックして修正・実行してください
struct Product {
    name: String,
    price: u32,
    stock: u32,
}

impl Product {
    // 商品名と価格を受け取り、在庫0のProductを返す関連関数newを定義せよ
    // 戻り値の型にはSelfを使うこと

}

fn main() {
    // 関連関数newを呼び出して、商品名「コーヒー豆」・価格1200の商品をproductに束縛せよ

    println!(
        "{}: {}円 / 在庫{}個",
        product.name, product.price, product.stock
    );
}
Playgroundで開く
解答例と解説
struct Product {
    name: String,
    price: u32,
    stock: u32,
}

impl Product {
    // 商品名と価格を受け取り、在庫0のProductを返す関連関数newを定義せよ
    // 戻り値の型にはSelfを使うこと
    fn new(name: String, price: u32) -> Self { 
        Product { 
            name, 
            price, 
            stock: 0, 
        } 
    } 
}

fn main() {
    // 関連関数newを呼び出して、商品名「コーヒー豆」・価格1200の商品をproductに束縛せよ
    let product = Product::new(String::from("コーヒー豆"), 1200); 

    println!(
        "{}: {}円 / 在庫{}個",
        product.name, product.price, product.stock
    );
}
Playgroundで開く

implブロックの中に書いた、selfを取らない関数を関連関数と呼びます。呼び出しはドット記法ではなく型名::関数名()の形です。インスタンスがまだ存在しない状態でも呼べるので、インスタンスを作る「コンストラクタ」として使われます。

String::from("...")Vec::new()も、まさにこの形の関連関数です。第6章から使ってきた::の正体がこれでした。

戻り値のSelfは、implの対象になっている型(ここではProduct)を指すエイリアスです。-> Productと書いても同じ意味ですが、Selfと書いておけば型名を変更したときに直す箇所が減るため、こちらが一般的です。

問題03のbuild_productと比べてみてください。やっていることはほぼ同じですが、関連関数にすると次の利点があります。

  • Productの作り方がimpl Productの中にまとまる
  • Product::newという名前で、何を作る関数なのかが明確になる
  • stock: 0のような初期値のルールを1箇所に閉じ込められる

3つめが特に重要です。「新しい商品の在庫は0から始まる」という決まりをnewの中に書いておけば、インスタンスを作るたびに書き手が気にする必要がなくなります。

07 - implブロックは分割できる

implブロックに関する問題です。
残高を増やすメソッドdepositと、残高を表示するメソッドshowを定義してください。ただし、すでに書かれているimplブロックには手を加えないこと

期待する出力
田中さんの残高: 5000円
「Playgroundで開く」をクリックして修正・実行してください
struct BankAccount {
    owner: String,
    balance: u32,
}

// このimplブロックには手を加えないこと
impl BankAccount {
    fn new(owner: String) -> Self {
        BankAccount { owner, balance: 0 }
    }
}

// 残高をamount円増やすメソッドdepositを定義せよ
// 「〇〇さんの残高: 〇〇円」を表示するメソッドshowを定義せよ


fn main() {
    let mut account = BankAccount::new(String::from("田中"));

    account.deposit(5000);
    account.show();
}
Playgroundで開く
解答例と解説
struct BankAccount {
    owner: String,
    balance: u32,
}

// このimplブロックには手を加えないこと
impl BankAccount {
    fn new(owner: String) -> Self {
        BankAccount { owner, balance: 0 }
    }
}

// 残高をamount円増やすメソッドdepositを定義せよ
// 「〇〇さんの残高: 〇〇円」を表示するメソッドshowを定義せよ
impl BankAccount { 
    fn deposit(&mut self, amount: u32) { 
        self.balance += amount; 
    } 

    fn show(&self) { 
        println!("{}さんの残高: {}円", self.owner, self.balance); 
    } 
} 

fn main() {
    let mut account = BankAccount::new(String::from("田中"));

    account.deposit(5000);
    account.show();
}
Playgroundで開く

既存のimpl BankAccountに手を加えられないなら、impl BankAccountをもう1つ書けばよい——というのが答えです。

1つの型に対するimplブロックは、1つにまとめる必要がありません。今回のように「関連関数のブロック」「メソッドのブロック」と役割で分けたり、ファイルが長くなったときに関心ごとに分けたりできます。分けても呼び出し方は何も変わりません。

もちろん1つのブロックにまとめても正しく動きます。数が増えたときの整理手段として使えると覚えておいてください。

分割しても守られる決まりが1つあります。同じ名前の関連関数・メソッドは、ブロックをまたいでも重複して定義できません

これはコンパイルエラー(E0592)
impl BankAccount {
    fn show(&self) {}
}

impl BankAccount {
    fn show(&self) {} // エラー: E0592(重複した定義)
}

ブロックが違えば別物として扱われる、ということはありません。あくまで「BankAccountに定義されている名前」の一覧が1つあるだけです。

また、implブロックを書けるのはその型を定義しているのと同じクレート内に限られます。標準ライブラリのVecStringに、自分のコードから直接メソッドを追加することはできません。

08 - 応用: 銀行口座を作る

第9章からの総復習として、構造体implブロックメソッド関連関数を組み合わせた問題です。
銀行口座を表す構造体BankAccountと、その関連関数・メソッドを実装してテストに合格させてください。

名前 種類 内容
new(owner: String) 関連関数 残高0の口座を作って返す
deposit(amount) メソッド 残高をamount円増やす
withdraw(amount) メソッド 残高が足りれば引き出してtrue、足りなければ何もせずfalseを返す
balance() メソッド 現在の残高を返す
summary() メソッド 田中さんの残高は5000円ですの形式の文字列を返す。呼び出した後は、その口座を二度と使えなくすること
「Playgroundで開く」をクリックしてTESTを実行してください
// 構造体BankAccountを定義せよ(フィールド: owner(String), balance(u32))

// implブロックにnew・deposit・withdraw・balance・summaryを定義せよ

#[test]
fn test_new_account() {
    let account = BankAccount::new(String::from("田中"));
    assert_eq!(account.balance(), 0);
}

#[test]
fn test_deposit() {
    let mut account = BankAccount::new(String::from("田中"));
    account.deposit(5000);
    account.deposit(3000);
    assert_eq!(account.balance(), 8000);
}

#[test]
fn test_withdraw() {
    let mut account = BankAccount::new(String::from("田中"));
    account.deposit(5000);
    assert_eq!(account.withdraw(2000), true);
    assert_eq!(account.balance(), 3000);
}

#[test]
fn test_withdraw_insufficient() {
    let mut account = BankAccount::new(String::from("田中"));
    account.deposit(1000);
    assert_eq!(account.withdraw(2000), false);
    assert_eq!(account.balance(), 1000); // 失敗しても残高は変わらない
}

#[test]
fn test_summary() {
    let mut account = BankAccount::new(String::from("鈴木"));
    account.deposit(5000);
    assert_eq!(account.summary(), "鈴木さんの残高は5000円です");
}
Playgroundで開く
解答例と解説
// 構造体BankAccountを定義せよ(フィールド: owner(String), balance(u32))
struct BankAccount { 
    owner: String, 
    balance: u32, 
} 

// implブロックにnew・deposit・withdraw・balance・summaryを定義せよ
impl BankAccount { 
    fn new(owner: String) -> Self { 
        BankAccount { owner, balance: 0 } 
    } 

    fn deposit(&mut self, amount: u32) { 
        self.balance += amount; 
    } 

    fn withdraw(&mut self, amount: u32) -> bool { 
        if amount > self.balance { 
            return false; 
        } 

        self.balance -= amount; 
        true
    } 

    fn balance(&self) -> u32 { 
        self.balance 
    } 

    fn summary(self) -> String { 
        format!("{}さんの残高は{}円です", self.owner, self.balance) 
    } 
} 

#[test]
fn test_new_account() {
    let account = BankAccount::new(String::from("田中"));
    assert_eq!(account.balance(), 0);
}

#[test]
fn test_deposit() {
    let mut account = BankAccount::new(String::from("田中"));
    account.deposit(5000);
    account.deposit(3000);
    assert_eq!(account.balance(), 8000);
}

#[test]
fn test_withdraw() {
    let mut account = BankAccount::new(String::from("田中"));
    account.deposit(5000);
    assert_eq!(account.withdraw(2000), true);
    assert_eq!(account.balance(), 3000);
}

#[test]
fn test_withdraw_insufficient() {
    let mut account = BankAccount::new(String::from("田中"));
    account.deposit(1000);
    assert_eq!(account.withdraw(2000), false);
    assert_eq!(account.balance(), 1000); // 失敗しても残高は変わらない
}

#[test]
fn test_summary() {
    let mut account = BankAccount::new(String::from("鈴木"));
    account.deposit(5000);
    assert_eq!(account.summary(), "鈴木さんの残高は5000円です");
}
Playgroundで開く

この章で学んだselfの3つの形式が、すべて登場しています。

名前 selfの形 理由
new なし(関連関数) インスタンスがまだ存在しない
deposit &mut self 残高を書き換える
withdraw &mut self 残高を書き換える
balance &self 残高を読むだけ
summary self 口座を消費して文字列に変換する

いくつか押さえておきたい点があります。

withdrawは引き算する前に判定する
u32は負の値を表せないので、残高より多い額を引くと実行時にプログラムが停止します(オーバーフロー)。第2章で学んだ整数型の範囲がここで効いてきます。先にamount > self.balanceを確認し、足りなければreturn falseで早期リターンして、残高には一切触れません。テストのtest_withdraw_insufficientは、まさにここを確認しています。

フィールドとメソッドは同じ名前を付けられる
balanceというフィールドとbalance()というメソッドが共存しています。Rustではフィールド名とメソッド名は別々に管理されるため衝突しません。self.balanceはフィールド、self.balance()はメソッドと、括弧の有無で区別されます。読み取り用のメソッドにフィールドと同じ名前を付けるのは、よくある書き方です。

summaryselfを取る意味
format!でフィールドを読むだけなら&selfでも書けます。あえてselfにしているのは「口座の内容をまとめたら、その口座はもう使わない」という意図を型で表すためです。テストでもaccount.summary()が最後の行になっていて、それ以降accountは使えません。

balanceメソッドを用意する理由
テストはaccount.balanceと直接フィールドを読まず、account.balance()というメソッドを通しています。今は同じ結果ですが、メソッドを窓口にしておけば、後から「残高の表示だけ手数料を差し引く」といった変更をしたくなったときにimplの中だけで対応できます。