// book chapter
動かして学ぶRustプログラミング問題集第9章 構造体.
ここからは、自分で型を作る話に入ります。
この章では、関連する値をひとまとめにして名前を付ける構造体を10問で身につけます。
第5章のタプルでも複数の値をまとめられましたが、.0・.1という番号でしか要素を区別できず、何番目が何なのかはコードを書いた本人しか分かりませんでした。構造体を使えば、それぞれの値に名前を付けたうえで、まとまり自体にも型としての名前を与えられます。
そして構造体は、第7章・第8章で学んだ所有権や借用のルールがそのまま適用される場所でもあります。「フィールドがムーブする」「構造体を借用する」といった話が何度も出てくるので、前の章の内容を思い出しながら進めてください。
進め方は第8章までと同じです。各問題の冒頭に関連する辞書へのリンクを挙げているので、まずはリンク先で必要な知識を確認してから取り組んでください。
01 - 構造体を定義する
構造体に関する問題です。
商品を表す構造体Productを定義し、インスタンスを作って各フィールドを出力してください。フィールドは次の3つです。
| フィールド名 | 型 | 値 |
|---|---|---|
name |
String |
コーヒー豆 |
price |
u32 |
1200 |
in_stock |
bool |
true |
商品名: コーヒー豆
価格: 1200円
在庫あり: true// 上の表のフィールド名と型で構造体Productを定義せよ
fn main() {
// 上の表の値でProductのインスタンスを作り、変数productに束縛せよ
println!("商品名: {}", product.name);
println!("価格: {}円", product.price);
println!("在庫あり: {}", product.in_stock);
}Playgroundで開く解答例と解説
// 上の表のフィールド名と型で構造体Productを定義せよ
struct Product {
name: String,
price: u32,
in_stock: bool,
}
fn main() {
// 上の表の値でProductのインスタンスを作り、変数productに束縛せよ
let product = Product {
name: String::from("コーヒー豆"),
price: 1200,
in_stock: true,
};
println!("商品名: {}", product.name);
println!("価格: {}円", product.price);
println!("在庫あり: {}", product.in_stock);
}Playgroundで開くstructキーワードで構造体を定義します。書くのはフィールドの名前と型だけで、値はまだ入れません。定義は「こういう形の型を作る」という設計図にあたるもので、実際のデータは持ちません。
struct 構造体名 {
フィールド名: 型,
フィールド名: 型,
}インスタンス(実際の値)を作るときは構造体名 { フィールド名: 値, ... }と書き、全フィールドに値を渡します。1つでも欠けるとコンパイルエラーです。値を取り出すときはproduct.nameのようにドットでフィールド名を指定します。
第5章のタプルと比べると、構造体の利点がはっきりします。
| タプル | 構造体 | |
|---|---|---|
| 型の書き方 | (String, u32, bool) |
Product |
| 要素の指定 | product.0 |
product.name |
| 定義の必要 | 不要 | 必要 |
タプルは定義なしですぐ使える手軽さがありますが、product.2が在庫の有無だと分かるのは書いた本人だけです。構造体はあらかじめ定義する手間がかかるかわりに、product.in_stockと書けば誰が読んでも意味が分かります。
02 - フィールドを書き換える
構造体と変数に関する問題です。
次のコードはコンパイルエラー(E0594)になります。mainの中の1行に1単語だけ足して修正してください。
コーヒー豆: 1320円struct Product {
name: String,
price: u32,
}
fn main() {
let product = Product {
name: String::from("コーヒー豆"),
price: 1200,
};
product.price = 1320;
println!("{}: {}円", product.name, product.price);
}Playgroundで開く解答例と解説
struct Product {
name: String,
price: u32,
}
fn main() {
let product = Product {
let mut product = Product { // mutを追加した
name: String::from("コーヒー豆"),
price: 1200,
};
product.price = 1320;
println!("{}: {}円", product.name, product.price);
}Playgroundで開くエラーメッセージはcannot assign to 'product.price', as 'product' is not declared as mutable(E0594)——「productは可変として宣言されていないので、product.priceに代入できない」です。
第1章で学んだ「変数はデフォルトで不変」というルールは、構造体のフィールドにもそのまま効きます。フィールドを書き換えたければ、インスタンスを束縛する変数にmutを付けます。
ここで注意したいのは、mutが付くのはインスタンス単位だという点です。「priceだけ可変にする」といった指定はできません。
struct Product {
name: String,
mut price: u32, // フィールドごとにmutは付けられない
}let mut productと書いた時点で、nameもpriceもどちらも書き換え可能になります。逆にmutを付けなければ、どのフィールドも書き換えられません。
03 - フィールド初期化省略記法
構造体と関数に関する問題です。
引数からProductを作って返す関数build_productの中身を書いてください。引数名とフィールド名が同じであることを活かした書き方があります。
紅茶: 420円struct Product {
name: String,
price: u32,
}
fn build_product(name: String, price: u32) -> Product {
// 引数nameとpriceからProductを作って返せ
}
fn main() {
let product = build_product(String::from("紅茶"), 420);
println!("{}: {}円", product.name, product.price);
}Playgroundで開く解答例と解説
struct Product {
name: String,
price: u32,
}
fn build_product(name: String, price: u32) -> Product {
// 引数nameとpriceからProductを作って返せ
Product { name, price } // name: name, price: price の省略形
}
fn main() {
let product = build_product(String::from("紅茶"), 420);
println!("{}: {}円", product.name, product.price);
}Playgroundで開くProduct { name: name, price: price }と書いても正解ですが、フィールド名と同じ名前の変数がその場にあるとき、フィールド名: 変数名をフィールド名だけに省略できます。これをフィールド初期化省略記法と呼びます。
| 書き方 | 意味 |
|---|---|
Product { name: name, price: price } |
省略しない形 |
Product { name, price } |
省略記法(同じ意味) |
省略できるのは名前が完全に一致するときだけです。Product { name, price: price * 2 }のように、一部だけ省略して残りは普通に書く、という混在もできます。
関数の引数名をフィールド名に合わせておくと、この記法がそのまま使えるため、構造体を作る関数では引数名をフィールド名と揃えるのが定番です。第10章で学ぶコンストラクタでも、この書き方が頻繁に登場します。
04 - 構造体のDebug出力
構造体とコンソール出力に関する問題です。
次のコードはコンパイルエラー(E0277)になります。構造体の定義に1行足すだけで修正できます。
Product { name: "コーヒー豆", price: 1200 }struct Product {
name: String,
price: u32,
}
fn main() {
let product = Product {
name: String::from("コーヒー豆"),
price: 1200,
};
println!("{:?}", product);
}Playgroundで開く解答例と解説
#[derive(Debug)] // この1行を追加した
struct Product {
name: String,
price: u32,
}
fn main() {
let product = Product {
name: String::from("コーヒー豆"),
price: 1200,
};
println!("{:?}", product);
}Playgroundで開くエラーメッセージは'Product' doesn't implement 'Debug'(E0277)です。続けてadd '#[derive(Debug)]' to 'Product' or manually 'impl Debug for Product'と、直し方まで書かれています。
{}はユーザーに見せるための表示、{:?}はプログラマがデバッグのために中身を覗くための表示です。第5章で配列やタプルを{:?}で出力したときは何もせずに使えましたが、自分で定義した構造体は、そのままでは{}でも{:?}でも出力できません。どう表示すべきかをコンパイラが知らないためです。
そこで定義の直前に#[derive(Debug)]と書きます。deriveは「導出する」という意味で、構造体の定義内容からデバッグ表示のコードをコンパイラに自動生成させる指定です。これで{:?}が使えるようになります。
{:#?}と書くと、フィールドごとに改行した見やすい形になります。フィールドの多い構造体ではこちらが便利です。
#[derive(Debug)]
struct Product {
name: String,
price: u32,
}
fn main() {
let product = Product { name: String::from("コーヒー豆"), price: 1200 };
println!("{product:?}"); // Product { name: "コーヒー豆", price: 1200 }
println!("{product:#?}"); // 改行して整形表示
}Playgroundで開く05 - 構造体更新記法
構造体更新記法と構造体に関する問題です。
standardをもとに、priceだけを1980に変えたインスタンスpremiumを作ってください。他のフィールドはstandardと同じ値です。
Plan { price: 980, days: 30, auto_renew: true }
Plan { price: 1980, days: 30, auto_renew: true }#[derive(Debug)]
struct Plan {
price: u32,
days: u32,
auto_renew: bool,
}
fn main() {
let standard = Plan {
price: 980,
days: 30,
auto_renew: true,
};
// standardをもとに、priceだけ1980にしたインスタンスpremiumを作れ
println!("{:?}", standard);
println!("{:?}", premium);
}Playgroundで開く解答例と解説
#[derive(Debug)]
struct Plan {
price: u32,
days: u32,
auto_renew: bool,
}
fn main() {
let standard = Plan {
price: 980,
days: 30,
auto_renew: true,
};
// standardをもとに、priceだけ1980にしたインスタンスpremiumを作れ
let premium = Plan {
price: 1980,
..standard // 残りのフィールドはstandardから引き継ぐ
};
println!("{:?}", standard);
println!("{:?}", premium);
}Playgroundで開く..standardと書くと、明示的に指定しなかったフィールドをstandardから引き継ぎます。これを構造体更新記法と呼びます。今回はdaysとauto_renewが引き継がれました。
フィールドが3つ程度なら全部書いてもさほど変わりませんが、フィールドが10個あって1つだけ違うインスタンスを作りたい、という場面では書く量が大きく変わります。
書く場所には決まりがあり、..baseは必ずフィールド列の最後に置きます。
| 書き方 | 可否 |
|---|---|
Plan { price: 1980, ..standard } |
○ |
Plan { ..standard, price: 1980 } |
×(構文エラー) |
引き継ぐフィールドを個別に選ぶことはできません。..baseは常に「まだ埋まっていないフィールドすべて」を担当します。
06 - 更新記法とムーブ
構造体更新記法とムーブに関する問題です。
次のコードはコンパイルエラー(E0382)になります。..regularという書き方は残したまま、largeを作った後もregularを使えるように修正してください。
コーヒー 250ml: 480円
コーヒー 400ml: 580円struct Drink {
name: String,
price: u32,
size_ml: u32,
iced: bool,
}
fn main() {
let regular = Drink {
name: String::from("コーヒー"),
price: 480,
size_ml: 250,
iced: false,
};
let large = Drink {
price: 580,
size_ml: 400,
..regular
};
println!("{} {}ml: {}円", regular.name, regular.size_ml, regular.price);
println!("{} {}ml: {}円", large.name, large.size_ml, large.price);
}Playgroundで開く解答例と解説
struct Drink {
name: String,
price: u32,
size_ml: u32,
iced: bool,
}
fn main() {
let regular = Drink {
name: String::from("コーヒー"),
price: 480,
size_ml: 250,
iced: false,
};
let large = Drink {
name: regular.name.clone(), // Stringのフィールドだけ明示してクローンする
price: 580,
size_ml: 400,
..regular // 残りのiced(bool)はコピーで引き継がれる
};
println!("{} {}ml: {}円", regular.name, regular.size_ml, regular.price);
println!("{} {}ml: {}円", large.name, large.size_ml, large.price);
}Playgroundで開くエラーメッセージはborrow of moved value: 'regular.name'(E0382)です。第7章で見たムーブのエラーですが、ムーブされたものがregularではなくregular.nameというフィールド単位で示されているところに注目してください。
..regularで引き継がれるフィールドは、第7章で学んだムーブの規則にそのまま従います。フィールドの型によって挙動が分かれるところがポイントです。
| フィールド | 型 | ..regularでの挙動 |
regular側のその後 |
|---|---|---|---|
name |
String |
ムーブ | 使用不可 |
iced |
bool |
コピー | 使用可 |
つまり..regularは「regularまるごと」をムーブするのではなく、実際に引き継いだフィールドだけをムーブします。今回はnameがムーブしてしまったため、後からregular.nameを読もうとしてエラーになりました。regular.priceやregular.size_mlは明示的に指定していて引き継がれていないので、そのまま読めます。
修正方法は、ムーブされて困るフィールドだけを明示的に指定することです。name: regular.name.clone()と書けばnameは..regularの担当から外れ、ムーブは起きません。残ったicedはboolなのでコピーされ、regularは無傷のまま残ります。
..regularを消して4フィールド全部を書いても動きますが、フィールドが増えたときに効いてくるのは今回の書き方です。「引き継ぎたいものは..baseに任せ、ムーブされると困るものだけ手前で処理する」と覚えてください。
07 - 構造体を参照で渡す
構造体・参照・借用に関する問題です。
商品を借りて商品名: 価格円の形式で表示する関数print_productを定義してください。所有権は奪わず、何度でも呼び出せるようにします。
コーヒー豆: 1200円
コーヒー豆: 1200円struct Product {
name: String,
price: u32,
}
// Productを借りて「名前: 価格円」を表示する関数print_productを定義せよ
fn main() {
let product = Product {
name: String::from("コーヒー豆"),
price: 1200,
};
print_product(&product);
print_product(&product); // 借りているだけなので何度でも渡せる
}Playgroundで開く解答例と解説
struct Product {
name: String,
price: u32,
}
// Productを借りて「名前: 価格円」を表示する関数print_productを定義せよ
fn print_product(product: &Product) {
println!("{}: {}円", product.name, product.price);
}
fn main() {
let product = Product {
name: String::from("コーヒー豆"),
price: 1200,
};
print_product(&product);
print_product(&product); // 借りているだけなので何度でも渡せる
}Playgroundで開く第8章で&Stringを受け取ったときとまったく同じです。自分で定義した型でも、参照の型は元の型に&を付けた&Productになります。
もし引数をproduct: Productにしていたら、1回目のprint_product(product)で所有権がムーブして、2回目の呼び出しがE0382になっていました。読むだけの関数は共有参照で受け取る、というRustの基本形は構造体でも変わりません。
注目したいのは関数の中身が変わらない点です。product.nameという書き方は、productがProductでも&Productでも同じように使えます。
struct Product {
name: String,
price: u32,
}
fn main() {
let product = Product { name: String::from("コーヒー豆"), price: 1200 };
let borrowed = &product;
println!("{}", borrowed.name); // *を書かなくてよい
println!("{}", (*borrowed).name); // これと同じ意味
}Playgroundで開く第8章の問題04で「メソッド呼び出しやフィールドアクセスでは参照外しが自動で行われる」と説明したのがこれです。参照越しにフィールドへアクセスするときは、*を自分で書く必要がありません。
08 - タプル構造体
タプル構造体に関する問題です。
お店の位置を表すタプル構造体Positionを定義してください。フィールドはi32が2つ(x座標・y座標)です。
x座標: 3
y座標: 5// i32を2つ持つタプル構造体Positionを定義せよ
fn main() {
let shop = Position(3, 5);
println!("x座標: {}", shop.0);
println!("y座標: {}", shop.1);
}Playgroundで開く解答例と解説
// i32を2つ持つタプル構造体Positionを定義せよ
struct Position(i32, i32);
fn main() {
let shop = Position(3, 5);
println!("x座標: {}", shop.0);
println!("y座標: {}", shop.1);
}Playgroundで開くフィールドに名前を付けず、型だけを並べて定義する構造体をタプル構造体と呼びます。波括弧ではなく丸括弧で書き、末尾にセミコロンが必要です。
インスタンスの作り方が独特で、Position(3, 5)という関数呼び出しの形になります。タプル構造体を定義すると、型名と同じ名前のコンストラクタが自動的に作られるためです。フィールドへのアクセスは、タプル型と同じく.0・.1と番号で行います。
第5章のタプル、この問題のタプル構造体、問題01からの構造体を並べると違いがはっきりします。
| フィールド名 | 型としての名前 | |
|---|---|---|
タプル (i32, i32) |
なし(位置) | なし |
タプル構造体 Position(i32, i32) |
なし(位置) | あり |
構造体 Position { x: i32, y: i32 } |
あり | あり |
タプル構造体は、この2つのちょうど中間にあたります。「フィールド名を付けるほどでもないけれど、値のまとまりに型としての名前は与えたい」という場面で使います。座標のようにx・yと書いても情報が増えない場合が典型例です。
09 - ニュータイプパターン
タプル構造体に関する問題です。
次のコードはコンパイルエラー(E0308)になります。mainの中を1箇所だけ変えて修正してください。
華氏: 77度struct Celsius(f64); // 摂氏
struct Fahrenheit(f64); // 華氏
fn to_fahrenheit(temperature: Celsius) -> Fahrenheit {
Fahrenheit(temperature.0 * 9.0 / 5.0 + 32.0)
}
fn main() {
let temperature = Fahrenheit(25.0);
let result = to_fahrenheit(temperature);
println!("華氏: {}度", result.0);
}Playgroundで開く解答例と解説
struct Celsius(f64); // 摂氏
struct Fahrenheit(f64); // 華氏
fn to_fahrenheit(temperature: Celsius) -> Fahrenheit {
Fahrenheit(temperature.0 * 9.0 / 5.0 + 32.0)
}
fn main() {
let temperature = Fahrenheit(25.0);
let temperature = Celsius(25.0); // 摂氏25度に修正した
let result = to_fahrenheit(temperature);
println!("華氏: {}度", result.0);
}Playgroundで開くエラーメッセージはmismatched types(E0308)で、引数の位置にexpected 'Celsius', found 'Fahrenheit'——「Celsiusを期待したのにFahrenheitが来た」と示されます。摂氏を華氏に変換する関数に、華氏の値を渡してしまっていました。
CelsiusもFahrenheitも、中身は同じf64が1つです。それでもRustは別の型として完全に区別します。フィールドが1つだけのタプル構造体で既存の型を包み、意味の違いを型として表すこの書き方をニュータイプパターンと呼びます。
第4章の問題08でも温度変換の関数を書きましたが、あのときの引数は素のf64でした。
fn to_fahrenheit(celsius: f64) -> f64 { ... }
let temperature = 25.0; // 摂氏なのか華氏なのか、値だけでは分からない
let result = to_fahrenheit(temperature);この形では、華氏の値を渡してもコンパイラは何も言いません。数値としてはどちらもf64だからです。間違いに気づけるのは、出力を見て「なんだかおかしい」と思ったときになります。ニュータイプパターンで包んでおけば、取り違えた時点でコンパイルエラーになります。
単位・IDの種類・税抜きと税込みの価格など、「同じ型だけれど混ぜてはいけない値」がコード上に複数あるときに効く手法です。実行時のコストはかからないので、迷ったら包んでおく価値があります。
10 - ユニット様構造体
ユニット様構造体に関する問題です。
フィールドを1つも持たない構造体Guestを定義してください。{:?}で出力できるようにもしてください。
Guest
会員登録なしでもご利用いただけます// フィールドを持たない構造体Guestを定義せよ({:?}で出力できるようにすること)
fn main() {
let visitor = Guest;
println!("{:?}", visitor);
println!("会員登録なしでもご利用いただけます");
}Playgroundで開く解答例と解説
// フィールドを持たない構造体Guestを定義せよ({:?}で出力できるようにすること)
#[derive(Debug)]
struct Guest;
fn main() {
let visitor = Guest;
println!("{:?}", visitor);
println!("会員登録なしでもご利用いただけます");
}Playgroundで開くstruct Guest;のようにフィールドリストごと省略した構造体をユニット様構造体と呼びます。保持する値がない点が、第4章で学んだユニット型()に似ているためこの名前が付いています。
インスタンスの作り方も独特で、型名をそのまま書くだけです。Guest { }ともGuest()とも書きません。定義すると型名と同じ名前の定数が暗黙に作られるため、let visitor = Guest;でインスタンスが得られます。
ここまでで、structキーワードで定義できる3つの形がそろいました。
| 定義 | 呼び名 | インスタンスの作り方 |
|---|---|---|
struct Product { name: String } |
構造体 | Product { name: ... } |
struct Position(i32, i32); |
タプル構造体 | Position(3, 5) |
struct Guest; |
ユニット様構造体 | Guest |
値を1つも持たない型に何の意味があるのか、と思うかもしれません。使いどころは振る舞いだけを持たせたい型です。次の章で学ぶメソッドはフィールドがなくても定義できるので、「状態は持たないが処理はまとめたい」という型をユニット様構造体で作れます。
#[derive(Debug)]
struct Guest;
fn main() {
println!("{}バイト", size_of::<Guest>()); // 0バイト
}Playgroundで開くフィールドを持たないので、実行時のメモリを1バイトも消費しません。型としての区別だけがコンパイル時に存在し、実行時には何も残らない、という使い方ができます。