// book chapter
動かして学ぶRustプログラミング問題集第16章 公開APIと外部パッケージ.
第15章で、コードを複数のファイルへ分け、ライブラリクレートとバイナリクレートを共存させられるようになりました。この章では、そうして組み上げたクレートを外からどう見せるか、そして他の人が書いたクレートをどう取り込むかを扱います。
第13章のpubは「見せるか見せないか」の指定でした。この章の主役であるpub useは、そこから一歩進んで「どのパスで見せるか」を決める仕組みです。内部のモジュール構造をそのまま外に押し付ける代わりに、使ってほしいものだけを短いパスで公開できます。
後半では外部パッケージを扱います。ここまで自分で書いてきたコードと、crates.ioから取ってくるクレートの使い方は、実はほとんど同じです。違うのはCargo.tomlへの1行だけです。
進め方は第15章までと同じです。各問題の冒頭に関連する辞書へのリンクを挙げているので、まずはリンク先で必要な知識を確認してから取り組んでください。
01 - pub useで再エクスポートする
再エクスポートとuse宣言に関する問題です。
Cartはsrc/shop/cart.rsにありますが、src/main.rsはplayground::Cartという短いパスで使おうとしています。src/lib.rsに1行足して、そのパスで使えるようにしてください。
お支払い金額: 2730円pub mod shop;
// ここに1行足せ(Cartをクレートルートから公開し直す)pub mod cart;pub struct Cart {
prices: Vec<u32>,
}
impl Cart {
pub fn new(prices: Vec<u32>) -> Self {
Cart { prices }
}
/// 送料込みの合計金額(3000円以上は送料無料)
pub fn total(&self) -> u32 {
let mut sum = 0;
for price in &self.prices {
sum += price;
}
if sum >= 3000 { sum } else { sum + 500 }
}
}use playground::Cart;
fn main() {
let cart = Cart::new(vec![980, 1250]);
println!("お支払い金額: {}円", cart.total());
}解答例と解説
pub mod shop;
// ここに1行足せ(Cartをクレートルートから公開し直す)
pub use crate::shop::cart::Cart; pub mod cart;pub struct Cart {
prices: Vec<u32>,
}
impl Cart {
pub fn new(prices: Vec<u32>) -> Self {
Cart { prices }
}
/// 送料込みの合計金額(3000円以上は送料無料)
pub fn total(&self) -> u32 {
let mut sum = 0;
for price in &self.prices {
sum += price;
}
if sum >= 3000 { sum } else { sum + 500 }
}
}use playground::Cart;
fn main() {
let cart = Cart::new(vec![980, 1250]);
println!("お支払い金額: {}円", cart.total());
}use宣言にpubを付けたpub useが再エクスポートです。第14章で学んだとおりuseは名前をそのスコープへ持ち込む宣言でしたが、pubが付くと、持ち込んだ名前をそのスコープの外へも公開します。
今回pub use crate::shop::cart::Cart;を書いた場所はクレートルートなので、Cartはクレートルートの名前としても公開されます。結果として、使う側はplayground::Cartと書けるようになります。
crate(ライブラリ)
├── shop
│ └── cart
│ └── Cart // 定義されている場所
└── Cart // pub use で開いた入口(同じ型を指す)
何が良いのか
この1行がないと、使う側は定義されている場所まで自分でたどることになります。
| 公開のしかた | 使う側が書くパス |
|---|---|
| 再エクスポートなし | use playground::shop::cart::Cart; |
| 再エクスポートあり | use playground::Cart; |
利点は短さだけではありません。使う側が書くパスと、作る側の内部構造が切り離されることが本質です。あとでcartを別のモジュールへ移しても、lib.rsのpub useの行を直せば済み、使う側のコードは無傷です。再エクスポートなしの場合は、内部を動かすたびに使う側が壊れます。
あくまで別名
pub useは型を移動させるわけではありません。Cartが定義されている場所はcrate::shop::cart::Cartのままで、playground::Cartはそこへの入口が1つ増えただけです。今回はpub mod shopと公開してあるので、use playground::shop::cart::Cart;と長いほうのパスで書いても動きます。この「長いほうの道」を塞ぐ方法が、次の問題02です。
02 - 内部モジュールを隠す
再エクスポートとpubに関する問題です。
次のプロジェクトはコンパイルエラー(E0603)になります。src/lib.rsは書き換えずに、src/main.rsの1行目を直して修正してください。
合計: 2230円// pubを付けていないので、priceモジュール自体は外から見えない
mod price;
// 使ってほしいものだけをクレートルートで公開する
pub use crate::price::Receipt;pub struct Receipt {
pub total: u32,
}
impl Receipt {
pub fn issue(prices: &[u32]) -> Self {
let mut sum = 0;
for price in prices {
sum += price;
}
Receipt { total: sum }
}
}use playground::price::Receipt;
fn main() {
let receipt = Receipt::issue(&[980, 1250]);
println!("合計: {}円", receipt.total);
}解答例と解説
// pubを付けていないので、priceモジュール自体は外から見えない
mod price;
// 使ってほしいものだけをクレートルートで公開する
pub use crate::price::Receipt;pub struct Receipt {
pub total: u32,
}
impl Receipt {
pub fn issue(prices: &[u32]) -> Self {
let mut sum = 0;
for price in prices {
sum += price;
}
Receipt { total: sum }
}
}use playground::price::Receipt;
use playground::Receipt;
fn main() {
let receipt = Receipt::issue(&[980, 1250]);
println!("合計: {}円", receipt.total);
}エラーはmodule 'price' is private(E0603)で、指されているのはパスのpriceの部分です。Receiptではありません。
lib.rsのmod price;にはpubが付いていません。第13章で学んだとおり、pubが付いていないモジュールはクレートの外から見えないので、playground::price::...というパスはpriceのところで止まります。問題01との違いはここだけです。
それなのにReceiptは使える
不思議に見えるかもしれません。priceが見えないのに、その中にあるReceiptはplayground::Receiptとして使えます。
crate(ライブラリ)
├── price // 非公開 → この道は通れない
│ └── Receipt // pub
└── Receipt // pub use で開いた入口 → こちらは通れる
再エクスポートが隠すのは経路であって、項目そのものではありません。pub useはプライバシーの連鎖を短絡させ、非公開のモジュールの中にある公開された項目へ、別の道を1本だけ開けます。開いていない道(playground::price::Receipt)は塞がったままです。
これが公開APIの設計
問題01の構成では、使う側は短いパスと長いパスのどちらでも書けました。この問題の構成では、短いパスしか存在しません。
| 構成 | 使う側が書けるパス | 内部を変更したとき |
|---|---|---|
pub mod price; のみ |
playground::price::Receipt |
使う側が壊れる |
pub mod price; + pub use |
両方 | 長いパスを使っていた側が壊れる |
mod price; + pub use |
playground::Receiptのみ |
壊れない |
いちばん下の形なら、priceというモジュール名を変えようが、中を2つに分割しようが、pub useの行さえ直せば外への約束は変わりません。外に見せたいものだけをpub useで通し、モジュール自体は非公開のままにするというのが、ライブラリを作るときの定番の形です。
pubが付いた項目しか通せない
price.rsのReceiptにはpubが付いています。ここを非公開にすると、同じクレートの中であってもpub useできずE0364になります。隠せるのは経路だけなので、通したい項目そのものにはpubが必要です。
03 - 外部パッケージを使う
外部パッケージとuse宣言に関する問題です。
次のコードはregexという外部のクレートを使って、商品コードの形式を判定します。Cargo.toml側の準備は済んでいるものとして、コードに1行足して動くようにしてください。
[package]
name = "playground"
version = "0.1.0"
edition = "2024"
[dependencies]
regex = "1"A-1234 は商品コードの形式か: true
12345 は商品コードの形式か: false// ここに1行足せ(regexクレートのRegexを持ち込む)
fn main() {
// 商品コード(英大文字1文字 + ハイフン + 数字4桁)の形式かどうかを判定する
let code_pattern = Regex::new(r"^[A-Z]-[0-9]{4}$").unwrap();
for code in ["A-1234", "12345"] {
println!("{code} は商品コードの形式か: {}", code_pattern.is_match(code));
}
}Playgroundで開く解答例と解説
// ここに1行足せ(regexクレートのRegexを持ち込む)
use regex::Regex;
fn main() {
// 商品コード(英大文字1文字 + ハイフン + 数字4桁)の形式かどうかを判定する
let code_pattern = Regex::new(r"^[A-Z]-[0-9]{4}$").unwrap();
for code in ["A-1234", "12345"] {
println!("{code} は商品コードの形式か: {}", code_pattern.is_match(code));
}
}Playgroundで開く自分で書いていない外部パッケージを使う手順は2段階です。
| 手順 | 書く場所 | 書くこと |
|---|---|---|
| 1. 依存の宣言 | Cargo.tomlの[dependencies] |
regex = "1" |
| 2. スコープへの持ち込み | .rsファイル |
use regex::Regex; |
1つ目はCargoへの指示です。これを見てCargoがcrates.ioからパッケージを取得し、コンパイルして、自分のクレートから使えるように渡してくれます。2つ目は第14章で学んだuseそのもので、外部のクレートだからといって特別な書き方はありません。
なぜ2段階なのか
2つは目的が別なので、忘れたときの症状も違います。
| 忘れたもの | どうなるか |
|---|---|
Cargo.tomlへの追記 |
regexというクレート名自体が解決できずコンパイルエラー |
use宣言 |
エラーにはならない。ただし毎回regex::Regex::new(..)とフルパスで書くことになる |
useは名前を短くするためのものなので、なくても書けます。実際、useを消してlet code_pattern = regex::Regex::new(..)と書いても動きます。第14章の慣習どおり、構造体などの型は項目そのものまで持ち込むのが一般的なので、ここではuse regex::Regex;としました。
バージョンの"1"の意味
regex = "1"は「1.0.0以上2.0.0未満」という要件です。この範囲であれば互換性が保たれる約束(セマンティックバージョニング)になっているので、細かい版を指定せずに書くのが普通です。手で書く代わりにcargo add regexと実行すれば、Cargoが適切な行を追記してくれます。
このコードが何をしているか
Regex::new(..)は正規表現をコンパイルしてResultを返します(第12章)。今回のパターンは文字列リテラルとして正しいことが分かっているので、unwrapで取り出しています。r"..."は生文字列リテラルで、\をエスケープせずに書ける記法です。正規表現そのものはRustの話ではないので、ここでは「外部のクレートを1つ使ってみる」という部分だけ押さえてください。
04 - 標準ライブラリは宣言いらず
外部パッケージとuse宣言に関する問題です。
次のコードはコンパイルエラー(E0433)になります。Cargo.tomlには何も足さずに、コードだけを直して修正してください。
りんごの在庫: 3個
みかんの在庫: 12個fn main() {
// 商品ごとの在庫数
let mut stock = HashMap::new();
stock.insert("りんご", 3);
stock.insert("みかん", 12);
println!("りんごの在庫: {}個", stock["りんご"]);
println!("みかんの在庫: {}個", stock["みかん"]);
}Playgroundで開く解答例と解説
use std::collections::HashMap;
fn main() {
// 商品ごとの在庫数
let mut stock = HashMap::new();
stock.insert("りんご", 3);
stock.insert("みかん", 12);
println!("りんごの在庫: {}個", stock["りんご"]);
println!("みかんの在庫: {}個", stock["みかん"]);
}Playgroundで開くエラーはuse of undeclared type 'HashMap'(E0433)です。HashMapは標準ライブラリのstd::collectionsにある型で、使うにはuse std::collections::HashMap;が要ります。
問題03の表と並べると、標準ライブラリの位置付けがはっきりします。
Cargo.tomlへの追記 |
use宣言 |
|
|---|---|---|
外部パッケージ(regexなど) |
必要 | 必要(フルパスで書くなら省略可) |
標準ライブラリ(std) |
不要 | 必要(フルパスで書くなら省略可) |
stdクレートは、特別な指定をしない限り常にコンパイラへ渡されます。そのため依存として宣言する必要がありません。ですが、渡されていることと、名前が短く書けることは別の話です。stdの中にある項目を裸の名前で使いたければ、外部パッケージとまったく同じようにuseが要ります。
StringやVecにuseが要らない理由
ここまでStringもVecもuseなしで使ってきました。これらはプレリュードと呼ばれる、あらかじめ自動で取り込まれる項目群に入っているためです。stdの中でも特によく使うものだけが選ばれていて、HashMapはそこに入っていません。
「useが要るかどうか」はプレリュードに入っているかどうかで決まる、と考えると混乱しません。
エラーメッセージが答えを教えてくれる
今回のエラーには、次のヒントが付いています。
help: consider importing this struct
|
1 + use std::collections::HashMap;
コンパイラがHashMapという名前を持つ項目を探し出して、必要なuseの行をそのまま提示してくれます。「型名は分かるがどこにあるか分からない」というときは、とりあえず書いてコンパイルし、この助言を読むのが速い方法です。
05 - 応用: ライブラリの公開APIを設計する
プロジェクト構成編(第13章〜第16章)の総復習です。
買い物カートのライブラリを4つのファイルに分けて実装し、テストに合格させてください。プロジェクトにはコメントだけを書いたファイルが用意してあります。src/lib.rsのテストコード以外は、すべて自分で書きます。
ファイル構成
src/
├── lib.rs … 公開APIの整備(テストは記述済み)
├── cart.rs … Cartの実装
├── price.rs … 送料の計算
└── price/
└── tax.rs … 消費税の計算
クレートの外から使える公開API
| パス | 内容 |
|---|---|
playground::Cart |
買い物カートの型 |
playground::with_tax |
税込価格を返す関数 |
内部のモジュール(cart・price・price::tax)は、クレートの外からは見えないようにしてください。
Cart(src/cart.rs)
| 名前 | 種類 | 内容 |
|---|---|---|
Item |
構造体 | name(String)・price(u32)。ライブラリの外には見せない |
Cart |
構造体 | items(Vec<Item>)。フィールドは非公開のままにする |
new() |
関連関数 | 空のカートを作って返す |
add(name, price) |
メソッド | 商品を1つ追加する |
count() |
メソッド | 入っている商品数をusizeで返す |
find_price(name) |
メソッド | 価格をOption<u32>で返す。なければNone |
total() |
メソッド | 小計に消費税、そのあと送料を足した支払い金額を返す |
価格の計算(src/price.rs・src/price/tax.rs)
| 名前 | 置き場所 | 内容 |
|---|---|---|
with_tax(price) |
price::tax |
消費税10%を足した金額(端数切り捨て) |
with_shipping(price) |
price |
3000円未満なら送料500円を足す。3000円以上はそのまま |
// 内部のモジュールを読み込め(クレートの外へは見せないこと)
// 公開APIを整えよ(Cartとwith_taxをクレートルートから使えるようにする)
#[cfg(test)]
mod tests {
use super::*;
fn sample() -> Cart {
let mut cart = Cart::new();
cart.add("コーヒー豆", 1200);
cart.add("マグカップ", 800);
cart
}
#[test]
fn test_count() {
let cart = sample();
assert_eq!(cart.count(), 2);
}
#[test]
fn test_find_price() {
let cart = sample();
assert_eq!(cart.find_price("コーヒー豆"), Some(1200));
assert_eq!(cart.find_price("ビール"), None);
}
#[test]
fn test_with_tax() {
assert_eq!(with_tax(1000), 1100);
assert_eq!(with_tax(105), 115);
}
#[test]
fn test_total_with_shipping() {
// 小計2000円 → 税込2200円 → 3000円未満なので送料500円
assert_eq!(sample().total(), 2700);
}
#[test]
fn test_total_free_shipping() {
let mut cart = sample();
cart.add("電気ケトル", 5000);
// 小計7000円 → 税込7700円 → 3000円以上なので送料無料
assert_eq!(cart.total(), 7700);
}
}// 商品を表す構造体Itemを定義せよ(name: String / price: u32)
// 買い物カートを表す構造体Cartを定義せよ(items: Vec<Item>)
// Cartのimplブロックにnew・add・count・find_price・totalを定義せよ
// totalはpriceモジュールの関数を使い、消費税 → 送料の順に足すこと// taxの中身を src/price/tax.rs から読み込め
// 送料の計算に使う定数を定義せよ(このモジュールの中だけで使う)
// 送料が無料になる金額: 3000
// 送料: 500
// 税込価格を受け取り、3000円未満なら送料を足して返す関数with_shippingを定義せよ// 消費税率の定数TAX_RATEを定義せよ(10)
// 価格を受け取り、消費税を足した金額(端数切り捨て)を返す関数with_taxを定義せよ解答例と解説
// 内部のモジュールを読み込め(クレートの外へは見せないこと)
mod cart;
mod price;
// 公開APIを整えよ(Cartとwith_taxをクレートルートから使えるようにする)
pub use crate::cart::Cart;
pub use crate::price::tax::with_tax;
#[cfg(test)]
mod tests {
use super::*;
fn sample() -> Cart {
let mut cart = Cart::new();
cart.add("コーヒー豆", 1200);
cart.add("マグカップ", 800);
cart
}
#[test]
fn test_count() {
let cart = sample();
assert_eq!(cart.count(), 2);
}
#[test]
fn test_find_price() {
let cart = sample();
assert_eq!(cart.find_price("コーヒー豆"), Some(1200));
assert_eq!(cart.find_price("ビール"), None);
}
#[test]
fn test_with_tax() {
assert_eq!(with_tax(1000), 1100);
assert_eq!(with_tax(105), 115);
}
#[test]
fn test_total_with_shipping() {
// 小計2000円 → 税込2200円 → 3000円未満なので送料500円
assert_eq!(sample().total(), 2700);
}
#[test]
fn test_total_free_shipping() {
let mut cart = sample();
cart.add("電気ケトル", 5000);
// 小計7000円 → 税込7700円 → 3000円以上なので送料無料
assert_eq!(cart.total(), 7700);
}
}// 商品を表す構造体Itemを定義せよ(name: String / price: u32)
struct Item {
name: String,
price: u32,
}
// 買い物カートを表す構造体Cartを定義せよ(items: Vec<Item>)
pub struct Cart {
items: Vec<Item>,
}
// Cartのimplブロックにnew・add・count・find_price・totalを定義せよ
// totalはpriceモジュールの関数を使い、消費税 → 送料の順に足すこと
impl Cart {
pub fn new() -> Self {
Cart { items: Vec::new() }
}
pub fn add(&mut self, name: &str, price: u32) {
self.items.push(Item {
name: String::from(name),
price,
});
}
pub fn count(&self) -> usize {
self.items.len()
}
pub fn find_price(&self, name: &str) -> Option<u32> {
for item in &self.items {
if item.name == name {
return Some(item.price);
}
}
None
}
pub fn total(&self) -> u32 {
let mut subtotal = 0;
for item in &self.items {
subtotal += item.price;
}
let taxed = crate::price::tax::with_tax(subtotal);
crate::price::with_shipping(taxed)
}
} // taxの中身を src/price/tax.rs から読み込め
pub mod tax;
// 送料の計算に使う定数を定義せよ(このモジュールの中だけで使う)
// 送料が無料になる金額: 3000
// 送料: 500
const FREE_SHIPPING_LINE: u32 = 3000;
const SHIPPING_FEE: u32 = 500;
// 税込価格を受け取り、3000円未満なら送料を足して返す関数with_shippingを定義せよ
pub fn with_shipping(price: u32) -> u32 {
if price >= FREE_SHIPPING_LINE {
price
} else {
price + SHIPPING_FEE
}
} // 消費税率の定数TAX_RATEを定義せよ(10)
const TAX_RATE: u32 = 10;
// 価格を受け取り、消費税を足した金額(端数切り捨て)を返す関数with_taxを定義せよ
pub fn with_tax(price: u32) -> u32 {
price * (100 + TAX_RATE) / 100
} プロジェクト構成編で学んだものが、ほぼすべて登場しました。
| 章 | この問題で使ったところ |
|---|---|
| 第9章・第10章 | Item・Cartの定義、newという関連関数、&self / &mut selfのメソッド |
| 第12章 | find_priceが返すOption<u32> |
| 第13章 | 型・メソッド・モジュールへのpubの付け方 |
| 第14章 | crate::price::tax::with_tax(..)というパス、use super::* |
| 第15章 | mod cart;によるファイル分割、price.rsとprice/tax.rsの2段構成 |
| 第16章 | pub useによる公開APIの整備 |
出来上がったモジュールツリーは次の形です。
crate(ライブラリ)
├── cart // 非公開
│ ├── Item // 非公開
│ └── Cart // pub
├── price // 非公開
│ ├── FREE_SHIPPING_LINE / SHIPPING_FEE // 非公開
│ ├── with_shipping // pub
│ └── tax // pub
│ ├── TAX_RATE // 非公開
│ └── with_tax // pub
├── Cart // pub use
├── with_tax // pub use
└── tests // use super::* でクレートルートの名前を拾う
pubは1段ずつ、必要なところにだけ
この問題でいちばん間違えやすいのがpubの付け方です。pubは1段しか開かないので、外から使われる経路上のものには1つずつ付ける必要があります。
| 対象 | pub |
理由 |
|---|---|---|
mod cart; / mod price; |
不要 | 外へはpub useで通すので、モジュール自体は隠す |
pub mod tax; |
必要 | pub use crate::price::tax::with_tax;がtaxを通る |
struct Item |
不要 | 外にも他のモジュールにも出さない |
pub struct Cart |
必要 | 公開APIの型 |
Cartのitems |
不要 | フィールドは非公開のまま。個数はcount()で見せる |
Cartの各メソッド |
必要 | 型を公開しても、メソッドは1つずつpubが要る |
pub fn with_shipping |
必要 | 兄弟モジュールのcartから呼ぶ |
定数(TAX_RATEなど) |
不要 | 同じモジュールの中だけで使う |
メソッドのpubは特に忘れがちです。pub struct Cartと書いてもメソッドは非公開のままなので、テストから呼ぶとE0624(method 'add' is private)になります。第13章の問題04で見た「構造体を公開してもフィールドは非公開」と同じ、1段ずつの原則です。
Itemと定数を非公開にできる理由
Itemはcartの中だけで使い、FREE_SHIPPING_LINEはpriceの中だけで使うので、どちらもpubが要りません。非公開のままにできたものが多いほど、あとから自由に変えられる部分が多いということです。たとえばItemにフィールドを1つ足しても、このライブラリを使う人には何の影響もありません。
with_shippingにだけpubが要る理由
同じ「内部で使うだけの関数」でも、with_shippingにはpubが必要でした。呼び出し元のcartがpriceの子孫ではないからです。第13章の問題06で確かめたとおり、非公開の項目が見えるのは、それを定義したモジュールとその子孫だけです。cartとpriceは兄弟なので、この関係にあてはまりません。
外に出したくないが同じクレートの中からは呼びたい、という意図をより正確に書くならpub(crate) fn with_shippingとします。今回はprice自体が非公開なので、pubでもクレートの外には漏れません。
パスの書き方
totalの中ではcrate::price::tax::with_tax(..)と絶対パスで書きました。第14章の問題01のとおり、絶対パスは書いている場所に依存しないので、既定の選択として無難です。useで短くするなら次のように書けます。
use crate::price::{self, tax};
// ...
pub fn total(&self) -> u32 {
let mut subtotal = 0;
for item in &self.items {
subtotal += item.price;
}
price::with_shipping(tax::with_tax(subtotal))
}use crate::price::{self, tax};は、第14章の問題03の表で挙げた波括弧の書き方です。selfがprice自身、taxがその子を指すので、1行で両方を持ち込めます。
テストが見ているもの
mod testsの中のuse super::*;が拾うのは、親であるクレートルートの名前です。cartやpriceの中身を直接見ているわけではありません。pub useを2行書いてはじめて、クレートルートにCartとwith_taxという名前ができ、テストがそれを使えるようになります。
裏を返せば、このテストは公開APIをそのまま使う形になっています。使う人が書くのと同じコードでテストしているので、公開APIの使い勝手もここで確かめられます。
テストでは守れない設計もある
mod cart;をpub mod cart;に変えても、テストは全部通ります。内部を隠せているかどうかは、テストでは検出できません。同じように、Itemにpubを付けても、with_shippingをpubのまま外に見せてもテストは通ります。
だからこそ、公開APIの形は自分で意識して決める必要があります。判断の基準はいつも同じで、「これは外との約束にしてよいか」です。約束にしたものは、あとから変えると使う側が壊れます。迷ったら隠しておき、必要になってからpub useで通すほうが安全です。