// book chapter
動かして学ぶRustプログラミング問題集第10章 メソッドと関連関数.
第9章では、自分で型を定義してデータをまとめられるようになりました。
この章では、その型に振る舞いを与える方法——implブロックによるメソッドと関連関数——を8問で身につけます。
ここまでString::from("...")やvec.push(3)のように、型に紐づいた関数をずっと使ってきました。String::fromのように型名から呼ぶものを関連関数、vec.pushのように値から呼ぶものをメソッドと呼びます。この章を終えれば、これらを自分の型にも定義できるようになります。
メソッドはただの関数と違い、第一引数にselfを取ります。このselfを&self・&mut self・selfのどれで受け取るかは、第8章で学んだ「読むだけか、書き換えるか、所有権ごと消費するか」という判断そのものです。所有権と借用の知識がここで効いてきます。
進め方は第9章までと同じです。各問題の冒頭に関連する辞書へのリンクを挙げているので、まずはリンク先で必要な知識を確認してから取り組んでください。
01 - メソッドを定義する
implブロックとメソッドに関する問題です。
在庫の合計金額(price × stock)を返すメソッドtotal_priceを定義してください。
コーヒー豆の在庫金額: 3600円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円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——「selfはself引数を持つメソッドの中でしか使えないキーワードだ」と説明が付きます。ヒントには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個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個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を自動で補います。
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
レシート: コーヒー ×2struct 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個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円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つあります。同じ名前の関連関数・メソッドは、ブロックをまたいでも重複して定義できません。
impl BankAccount {
fn show(&self) {}
}
impl BankAccount {
fn show(&self) {} // エラー: E0592(重複した定義)
}ブロックが違えば別物として扱われる、ということはありません。あくまで「BankAccountに定義されている名前」の一覧が1つあるだけです。
また、implブロックを書けるのはその型を定義しているのと同じクレート内に限られます。標準ライブラリのVecやStringに、自分のコードから直接メソッドを追加することはできません。
08 - 応用: 銀行口座を作る
第9章からの総復習として、構造体・implブロック・メソッド・関連関数を組み合わせた問題です。
銀行口座を表す構造体BankAccountと、その関連関数・メソッドを実装してテストに合格させてください。
| 名前 | 種類 | 内容 |
|---|---|---|
new(owner: String) |
関連関数 | 残高0の口座を作って返す |
deposit(amount) |
メソッド | 残高をamount円増やす |
withdraw(amount) |
メソッド | 残高が足りれば引き出してtrue、足りなければ何もせずfalseを返す |
balance() |
メソッド | 現在の残高を返す |
summary() |
メソッド | 田中さんの残高は5000円ですの形式の文字列を返す。呼び出した後は、その口座を二度と使えなくすること |
// 構造体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()はメソッドと、括弧の有無で区別されます。読み取り用のメソッドにフィールドと同じ名前を付けるのは、よくある書き方です。
summaryがselfを取る意味
format!でフィールドを読むだけなら&selfでも書けます。あえてselfにしているのは「口座の内容をまとめたら、その口座はもう使わない」という意図を型で表すためです。テストでもaccount.summary()が最後の行になっていて、それ以降accountは使えません。
balanceメソッドを用意する理由
テストはaccount.balanceと直接フィールドを読まず、account.balance()というメソッドを通しています。今は同じ結果ですが、メソッドを窓口にしておけば、後から「残高の表示だけ手数料を差し引く」といった変更をしたくなったときにimplの中だけで対応できます。