Progrust Library.

// 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
「Playgroundで開く」をクリックして修正・実行してください
// 上の表のフィールド名と型で構造体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円
「Playgroundで開く」をクリックして修正・実行してください
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と書いた時点で、namepriceもどちらも書き換え可能になります。逆にmutを付けなければ、どのフィールドも書き換えられません。

03 - フィールド初期化省略記法

構造体関数に関する問題です。
引数からProductを作って返す関数build_productの中身を書いてください。引数名とフィールド名が同じであることを活かした書き方があります。

期待する出力
紅茶: 420円
「Playgroundで開く」をクリックして修正・実行してください
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 }
「Playgroundで開く」をクリックして修正・実行してください
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 }
「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を作れ

    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から引き継ぎます。これを構造体更新記法と呼びます。今回はdaysauto_renewが引き継がれました。

フィールドが3つ程度なら全部書いてもさほど変わりませんが、フィールドが10個あって1つだけ違うインスタンスを作りたい、という場面では書く量が大きく変わります。

書く場所には決まりがあり、..baseは必ずフィールド列の最後に置きます。

書き方 可否
Plan { price: 1980, ..standard }
Plan { ..standard, price: 1980 } ×(構文エラー)

引き継ぐフィールドを個別に選ぶことはできません。..baseは常に「まだ埋まっていないフィールドすべて」を担当します。

06 - 更新記法とムーブ

構造体更新記法ムーブに関する問題です。
次のコードはコンパイルエラー(E0382)になります。..regularという書き方は残したまま、largeを作った後もregularを使えるように修正してください。

期待する出力
コーヒー 250ml: 480円
コーヒー 400ml: 580円
「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 {
        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.priceregular.size_mlは明示的に指定していて引き継がれていないので、そのまま読めます。

修正方法は、ムーブされて困るフィールドだけを明示的に指定することです。name: regular.name.clone()と書けばname..regularの担当から外れ、ムーブは起きません。残ったicedboolなのでコピーされ、regularは無傷のまま残ります。

..regularを消して4フィールド全部を書いても動きますが、フィールドが増えたときに効いてくるのは今回の書き方です。「引き継ぎたいものは..baseに任せ、ムーブされると困るものだけ手前で処理する」と覚えてください。

07 - 構造体を参照で渡す

構造体参照借用に関する問題です。
商品を借りて商品名: 価格円の形式で表示する関数print_productを定義してください。所有権は奪わず、何度でも呼び出せるようにします。

期待する出力
コーヒー豆: 1200円
コーヒー豆: 1200円
「Playgroundで開く」をクリックして修正・実行してください
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という書き方は、productProductでも&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
「Playgroundで開く」をクリックして修正・実行してください
// 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つのちょうど中間にあたります。「フィールド名を付けるほどでもないけれど、値のまとまりに型としての名前は与えたい」という場面で使います。座標のようにxyと書いても情報が増えない場合が典型例です。

09 - ニュータイプパターン

タプル構造体に関する問題です。
次のコードはコンパイルエラー(E0308)になります。mainの中を1箇所だけ変えて修正してください。

期待する出力
華氏: 77度
「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 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が来た」と示されます。摂氏を華氏に変換する関数に、華氏の値を渡してしまっていました。

CelsiusFahrenheitも、中身は同じf64が1つです。それでもRustは別の型として完全に区別します。フィールドが1つだけのタプル構造体で既存の型を包み、意味の違いを型として表すこの書き方をニュータイプパターンと呼びます。

第4章の問題08でも温度変換の関数を書きましたが、あのときの引数は素のf64でした。

第4章の書き方
fn to_fahrenheit(celsius: f64) -> f64 { ... }

let temperature = 25.0; // 摂氏なのか華氏なのか、値だけでは分からない
let result = to_fahrenheit(temperature);

この形では、華氏の値を渡してもコンパイラは何も言いません。数値としてはどちらもf64だからです。間違いに気づけるのは、出力を見て「なんだかおかしい」と思ったときになります。ニュータイプパターンで包んでおけば、取り違えた時点でコンパイルエラーになります

単位・IDの種類・税抜きと税込みの価格など、「同じ型だけれど混ぜてはいけない値」がコード上に複数あるときに効く手法です。実行時のコストはかからないので、迷ったら包んでおく価値があります。

10 - ユニット様構造体

ユニット様構造体に関する問題です。
フィールドを1つも持たない構造体Guestを定義してください。{:?}で出力できるようにもしてください。

期待する出力
Guest
会員登録なしでもご利用いただけます
「Playgroundで開く」をクリックして修正・実行してください
// フィールドを持たない構造体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つも持たない型に何の意味があるのか、と思うかもしれません。使いどころは振る舞いだけを持たせたい型です。次の章で学ぶメソッドはフィールドがなくても定義できるので、「状態は持たないが処理はまとめたい」という型をユニット様構造体で作れます。

サイズは0バイト
#[derive(Debug)]
struct Guest;

fn main() {
    println!("{}バイト", size_of::<Guest>()); // 0バイト
}
Playgroundで開く

フィールドを持たないので、実行時のメモリを1バイトも消費しません。型としての区別だけがコンパイル時に存在し、実行時には何も残らない、という使い方ができます。