// book chapter
動かして学ぶRustプログラミング問題集第13章 モジュールと公開範囲.
第12章までで、Rustの基本文法・所有権・構造体・列挙型・OptionとResultを一通り学びました。ここからの第13章〜第16章はプロジェクト構成編です。書けるコードが増えてきたときに、それをどう整理し、どう分割し、どこまでを外に見せるかを扱います。
その出発点がこの章のモジュールです。関数や構造体が増えてくると、1つのファイルにただ並べるだけでは「どれとどれが関係しているのか」が読み取れなくなります。モジュールは関連するものをまとめて名前を付ける入れ物で、これがコードの整理の単位になります。
そしてモジュールにはもう1つ、より重要な役割があります。外から触れる範囲を決める壁になることです。Rustでは書いたものはすべて既定で非公開で、外に見せたいものだけをpubで1つずつ開けていきます。「この関数は内部の都合で作ったものだから外からは使わせない」という意図を、コメントではなくコンパイラが守るルールとして書けます。
この章はインラインモジュール(1つのファイルの中に{}で書くモジュール)だけを扱うので、これまでどおり1つのコードブロックで完結します。ファイルへの分割は第15章です。
進め方は第12章までと同じです。各問題の冒頭に関連する辞書へのリンクを挙げているので、まずはリンク先で必要な知識を確認してから取り組んでください。
01 - モジュールを定義する
モジュールに関する問題です。
greetingモジュールの中にhelloとbyeの2つの関数を定義して、mainから呼び出せるようにしてください。
こんにちは、太郎さん
さようなら、太郎さんmod greeting {
// 次の2つの関数をこの中に定義せよ(どちらも引数は名前の&str)
// hello: 「こんにちは、〇〇さん」と出力する
// bye : 「さようなら、〇〇さん」と出力する
// どちらもfnの前にpubを付けること(pubの意味は問題02で扱う)
}
fn main() {
greeting::hello("太郎");
greeting::bye("太郎");
}Playgroundで開く解答例と解説
mod greeting {
// 次の2つの関数をこの中に定義せよ(どちらも引数は名前の&str)
// hello: 「こんにちは、〇〇さん」と出力する
// bye : 「さようなら、〇〇さん」と出力する
// どちらもfnの前にpubを付けること(pubの意味は問題02で扱う)
pub fn hello(name: &str) {
println!("こんにちは、{name}さん");
}
pub fn bye(name: &str) {
println!("さようなら、{name}さん");
}
}
fn main() {
greeting::hello("太郎");
greeting::bye("太郎");
}Playgroundで開くモジュールは、mod 名前 { ... }と書いて作る入れ物です。中には関数・構造体・列挙型・定数など、これまで書いてきたものを何でも入れられます。そして中の項目を外から呼ぶときは、greeting::hello("太郎")のように::でモジュール名から順にたどります。
この::は初めて見る記号ではありません。第10章のInventory::new()、第11章のDrink::Coffee、第12章のString::fromと、ずっと使ってきた記号です。どれも「この名前の中にある、この名前」をたどる書き方で、モジュールの場合もまったく同じです。
モジュールツリー
モジュールはツリーを作ります。今回のコードなら次の形です。
crate // クレートルート(ファイルのいちばん外側)
├── greeting
│ ├── hello
│ └── bye
└── main
頂点にあるcrateはクレートルートと呼ばれ、モジュールで囲っていないコード(ここではmod greetingとfn main)が置かれている場所です。第16章まで、このツリーの図が何度も出てきます。
名前がぶつからなくなる
モジュールに分けるいちばん分かりやすい利点は、名前の衝突がなくなることです。同じファイルにfn helloを2つ書くとコンパイルエラーになりますが、別々のモジュールに入っていれば共存できます。
mod greeting {
pub fn hello() {}
}
mod farewell {
pub fn hello() {} // greeting::helloとは別物
}呼ぶ側はgreeting::hello()とfarewell::hello()で区別します。プログラムが大きくなるほど、この「名前の置き場所を分けられる」という性質が効いてきます。
02 - モジュールの中は非公開
pubとモジュールに関する問題です。
次のコードはコンパイルエラー(E0603)になります。エラーメッセージを読み、1語だけ足して修正してください。
本店 10:00-20:00mod shop {
fn open() {
println!("本店 10:00-20:00");
}
}
fn main() {
shop::open();
}Playgroundで開く解答例と解説
mod shop {
fn open() {
pub fn open() {
println!("本店 10:00-20:00");
}
}
fn main() {
shop::open();
}Playgroundで開くエラーメッセージはfunction 'open' is private(E0603)です。「openは非公開だ」と言われています。
Rustでは、モジュールの中に書いた項目はすべて既定で非公開です。パスが正しく書けていても、非公開のものには外から触れません。外に見せたいものにはpubを付けます。
これは他の多くの言語と逆向きの既定値です。何も指定しなければ外から使えてしまう言語では、「外から使われては困るもの」を隠す作業が必要になります。Rustは逆で、既定で全部隠れているところから、公開したいものだけを足し算で開けていきます。
なお、openにはpubが必要なのに、それを囲むshopのほうには付けなくてもmainから見えています。この理由は次の問題03で扱います。
なぜ既定が非公開なのか
非公開だと分かっている関数は、いつでも自由に書き換えられます。名前を変えても、引数を増やしても、消してしまっても、影響が及ぶのはそのモジュールの中だけだからです。一方、pubを付けた瞬間にそれは外との約束になり、変更すると外側のコードが壊れます。既定が非公開であることで、pubを書いた場所がそのまま「ここが外との境界です」という宣言になります。
エラーの読み方
Playgroundのエラー出力には、非公開である事実に加えてprivate function defined hereのように定義位置も示されます。E0603が出たら、まずpubの付け忘れを疑ってください。
03 - pubが開けるのは1段だけ
pubとモジュールに関する問題です。
次のコードはpub mod cartと書いてあるのにコンパイルエラー(E0603)になります。エラーメッセージを読んで修正してください。
合計: 2230円mod shop {
pub mod cart {
fn total(prices: &[u32]) -> u32 {
let mut sum = 0;
for price in prices {
sum += price;
}
sum
}
}
}
fn main() {
let prices = [980, 1250];
println!("合計: {}円", shop::cart::total(&prices));
}Playgroundで開く解答例と解説
mod shop {
pub mod cart {
fn total(prices: &[u32]) -> u32 {
pub fn total(prices: &[u32]) -> u32 {
let mut sum = 0;
for price in prices {
sum += price;
}
sum
}
}
}
fn main() {
let prices = [980, 1250];
println!("合計: {}円", shop::cart::total(&prices));
}Playgroundで開くエラーは今回もfunction 'total' is private(E0603)で、指されているのはcartではなくtotalです。
pubが開けるのは常に1段だけです。pub mod cartは「cartというモジュールの存在を外から見えるようにする」だけで、その中身までは公開しません。中のtotalを使わせたいなら、totalにもpubが要ります。
crate
└── shop // 非公開(でもmainと同じ場所にあるので見える。後述)
└── cart // pub → 見える
└── total // 非公開 → ここで止まる
パスは1セグメントずつ検査される
shop::cart::totalというパスは、shop・cart・totalの3つのセグメントに分かれます。可視性はこの1つずつについて確認され、どこか1つでも見えなければアクセスできません。ドアを1枚開けても、その奥にもう1枚閉まったドアがあれば通れないのと同じです。
mod shopにpubが要らない理由
問題02から持ち越した疑問がここで解けます。shopにはpubが付いていないのに、mainからは見えています。非公開の項目にアクセスできるのは「それを定義したモジュールとその子孫」で、shopを定義しているのはクレートルート、mainがいるのもクレートルートだからです。同じ場所にいるものからは、非公開でも見えます。
見えなくなるのは、外側から内側をのぞこうとしたときです。cartから見てtotalは内側にあるのでshopからは見えず、shopから見てcartの中身も内側なので見えません。この「内側は隠れる」という一方通行の関係を、問題06でもう一度確かめます。
04 - 構造体のフィールドは非公開のまま
pubと構造体とメソッドに関する問題です。
次のコードはコンパイルエラー(E0616)になります。issuerフィールドにはpubを付けずに(非公開のままにして)、その値を外から読み取れるように修正してください。
合計: 2230円
発行: レジ1mod shop {
pub struct Receipt {
pub total: u32,
issuer: String, // issuerフィールドにpubを付けて解決してはならない
}
impl Receipt {
// 必要ならここにメソッドを足してよい
}
pub fn checkout(prices: &[u32]) -> Receipt {
let mut total = 0;
for price in prices {
total += price;
}
Receipt {
total,
issuer: String::from("レジ1"),
}
}
}
fn main() {
let receipt = shop::checkout(&[980, 1250]);
println!("合計: {}円", receipt.total);
println!("発行: {}", receipt.issuer);
}Playgroundで開く解答例と解説
mod shop {
pub struct Receipt {
pub total: u32,
issuer: String, // issuerフィールドにpubを付けて解決してはならない
}
impl Receipt {
// 必要ならここにメソッドを足してよい
pub fn issuer(&self) -> &str {
&self.issuer
}
}
pub fn checkout(prices: &[u32]) -> Receipt {
let mut total = 0;
for price in prices {
total += price;
}
Receipt {
total,
issuer: String::from("レジ1"),
}
}
}
fn main() {
let receipt = shop::checkout(&[980, 1250]);
println!("合計: {}円", receipt.total);
println!("発行: {}", receipt.issuer);
println!("発行: {}", receipt.issuer());
}Playgroundで開くエラーはfield 'issuer' of struct 'Receipt' is private(E0616)です。
問題03の「1段だけ」は、モジュールだけの話ではありません。pub struct Receiptが公開したのは構造体そのもの、つまり「Receiptという型が外にあります」ということだけで、フィールドは1つずつpubを付けない限り非公開のままです。だからtotalは読めてissuerは読めない、という状態になります。
なぜフィールドにpubを付けさせなかったのか
issuerにもpubを付ければエラー自体は消えます。それを禁止してメソッドを書かせたのは、フィールドを公開すると構造体の中身がそのまま外との約束になってしまうからです。pubなフィールドは外から読めるだけでなく、mutな変数なら書き換えもできます。後からissuer: Stringをissuer: Vec<String>に変えたくなったとき、公開していたら外側のコードがすべて壊れます。
メソッド経由なら、外との約束は「issuer()を呼ぶと&strが返る」という1点だけです。中の持ち方を変えても、メソッドの中身を直せば外は無傷で済みます。
メソッド名とフィールド名は同じでよい
issuerというフィールドとissuer()というメソッドは共存できます。フィールドとメソッドは別の名前空間にあり、receipt.issuer(フィールド)とreceipt.issuer()(メソッド呼び出し)は書き方で区別が付くためです。読み取り用メソッドにフィールドと同じ名前を付けるのは、Rustでよく使われる書き方です。
戻り値が&strである理由
fn issuer(&self) -> StringにするとStringのクローンが必要になります。第8章で学んだとおり、読み取るだけなら参照を返せば十分です。&self.issuerは&Stringですが、&strを返す関数の中に書けば自動で&strに変換されます。
05 - 列挙型のバリアントは全部公開
pubと列挙型に関する問題です。
shopモジュールの中に、支払い方法を表す列挙型Paymentを定義してください。
支払い方法: 現金
支払い方法: カード
支払い方法: QR決済mod shop {
// Cash・Card・QrCodeの3つのバリアントを持つ列挙型Paymentを定義せよ
// モジュールの外から使えるようにすること
pub fn label(payment: &Payment) -> &str {
match payment {
Payment::Cash => "現金",
Payment::Card => "カード",
Payment::QrCode => "QR決済",
}
}
}
fn main() {
println!("支払い方法: {}", shop::label(&shop::Payment::Cash));
println!("支払い方法: {}", shop::label(&shop::Payment::Card));
println!("支払い方法: {}", shop::label(&shop::Payment::QrCode));
}Playgroundで開く解答例と解説
mod shop {
// Cash・Card・QrCodeの3つのバリアントを持つ列挙型Paymentを定義せよ
// モジュールの外から使えるようにすること
pub enum Payment {
Cash,
Card,
QrCode,
}
pub fn label(payment: &Payment) -> &str {
match payment {
Payment::Cash => "現金",
Payment::Card => "カード",
Payment::QrCode => "QR決済",
}
}
}
fn main() {
println!("支払い方法: {}", shop::label(&shop::Payment::Cash));
println!("支払い方法: {}", shop::label(&shop::Payment::Card));
println!("支払い方法: {}", shop::label(&shop::Payment::QrCode));
}Playgroundで開くpubはenumの前に1つ書くだけで、Cash・Card・QrCodeのそれぞれにpubは要りません。書こうとしても「不要だ」と警告されます。
問題04の構造体と並べると、違いがはっきりします。
pubを付ける場所 |
中身 | |
|---|---|---|
| 構造体 | 型とフィールドに1つずつ | フィールドは非公開のまま |
| 列挙型 | 型に1つだけ | バリアントはすべて公開 |
なぜ列挙型だけ違うのか
バリアントを隠した列挙型には、そもそも使い道がないためです。第11章で学んだとおり、列挙型はmatchで分岐して使うものであり、matchは網羅性チェックのために全バリアントを知っている必要があります。一部のバリアントが見えない列挙型は、外からmatchできません。
構造体は逆で、フィールドの一部だけを見せる形に意味があります。「合計金額は見せるが、どのレジで発行したかは見せない」という設計が成立するからです。pubの効き方の違いは、この使われ方の違いから来ています。
&Paymentで受け取っている理由
labelは表示名を返すだけで、値を消費する必要がありません。第8章で学んだとおり、読み取るだけなら参照で借りるのが基本です。所有権を取るpayment: Paymentにすると、main側で同じ値を2回使えなくなります。
なお、match paymentのpaymentは&Paymentなのに、パターンにはPayment::Cashと参照でない形を書いています。第11章の応用問題と同じで、コンパイラが自動で参照をたどってくれます。
06 - 子は祖先の非公開が見える
モジュールとpubに関する問題です。
cart::checkoutの中身を書いて、税込価格を返してください。
お支払い金額: 2200円mod shop {
const TAX_RATE: u32 = 10;
fn with_tax(price: u32) -> u32 {
price * (100 + TAX_RATE) / 100
}
pub mod cart {
pub fn checkout(price: u32) -> u32 {
// 親モジュールshopの非公開関数with_taxを呼び出し、税込価格を返せ
// shopの中の項目は crate::shop::項目名 でたどれる
// (パスの書き方そのものは第14章で詳しく扱う)
}
}
}
fn main() {
println!("お支払い金額: {}円", shop::cart::checkout(2000));
}Playgroundで開く解答例と解説
mod shop {
const TAX_RATE: u32 = 10;
fn with_tax(price: u32) -> u32 {
price * (100 + TAX_RATE) / 100
}
pub mod cart {
pub fn checkout(price: u32) -> u32 {
// 親モジュールshopの非公開関数with_taxを呼び出し、税込価格を返せ
// shopの中の項目は crate::shop::項目名 でたどれる
// (パスの書き方そのものは第14章で詳しく扱う)
crate::shop::with_tax(price)
}
}
}
fn main() {
println!("お支払い金額: {}円", shop::cart::checkout(2000));
}Playgroundで開くwith_taxにもTAX_RATEにもpubは付いていません。それでもcartの中からは呼べます。
非公開の項目にアクセスできるのは、それを定義したモジュールとその子孫です。with_taxを定義しているのはshopで、cartはその子なので、shopの非公開の項目がすべて見えます。
crate
└── shop
├── TAX_RATE // 非公開 ┐
├── with_tax // 非公開 ┼─ cart からは全部見える
└── cart // pub ┘
逆向きは見えない
親から子の非公開の項目は見えません。shopの中からcartの非公開の関数を呼ぼうとすると、外から呼んだときと同じE0603になります。
mod shop {
pub mod cart {
fn secret() -> u32 {
42
}
}
pub fn call() -> u32 {
// エラー: E0603(親でも子の非公開には触れない)
cart::secret()
}
}
fn main() {
println!("{}", shop::call());
}つまり可視性は外から内へは閉じていて、内から外へは開いているという一方通行の関係になっています。ファイルシステムの権限のように上下対称ではないので、そこだけ注意してください。
この性質が効く場面
モジュールの中に補助的な処理をまとめて隠しておき、子モジュールからは自由に使う、という書き方ができます。今回のwith_taxとTAX_RATEがまさにそれで、税率の計算はshopの内部事情なので外には見せず、shopの中ではcartからもcheckoutからも使えます。
そしてこの性質は、第14章の最後で扱うテストモジュールの土台でもあります。#[cfg(test)] mod testsという子モジュールを作れば、そこから親の非公開の関数をテストできます。「非公開のものはテストできない」という制約がRustにないのは、この規則のおかげです。