構造体を使ったプログラム例

構造体を使用したくなる可能性のあるケースを理解するために、長方形の面積を求めるプログラムを書きましょう。 単一の変数から始め、代わりに構造体を使うようにプログラムをリファクタリングします。

Cargoでrectanglesという新規バイナリプロジェクトを作成しましょう。このプロジェクトは、 長方形の幅と高さをピクセルで指定し、その面積を求めます。リスト5-8に、プロジェクトのsrc/main.rsで、 正にそうする一例を短いプログラムとして示しました。

ファイル名: src/main.rs

fn main() {
    let width1 = 30;
    let height1 = 50;

    println!(
        // 長方形の面積は、{}平方ピクセルです
        "The area of the rectangle is {} square pixels.",
        area(width1, height1)
    );
}

fn area(width: u32, height: u32) -> u32 {
    width * height
}

リスト5-8: 個別の幅と高さ変数を指定して長方形の面積を求める

では、cargo runでこのプログラムを走らせてください:

The area of the rectangle is 1500 square pixels.
(長方形の面積は、1500平方ピクセルです)

タプルでリファクタリングする

リスト5-8のコードはうまく動き、各寸法を与えてarea関数を呼び出すことで長方形の面積を割り出しますが、 改善点があります。幅と高さは、組み合わせると一つの長方形を表すので、相互に関係があるわけです。

このコードの問題点は、areaのシグニチャから明らかです:

fn area(width: u32, height: u32) -> u32 {

area関数は、1長方形の面積を求めるものと考えられますが、今書いた関数には、引数が2つあります。 引数は関連性があるのに、このプログラム内のどこにもそのことは表現されていません。 幅と高さを一緒にグループ化する方が、より読みやすく、扱いやすくなるでしょう。 それをする一つの方法については、第3章の「タプル型」節ですでに議論しました: タプルを使うのです。

タプルでリファクタリングする

リスト5-9は、タプルを使う別バージョンのプログラムを示しています。

ファイル名: src/main.rs

fn main() {
    let rect1 = (30, 50);

    println!(
        "The area of the rectangle is {} square pixels.",
        area(rect1)
    );
}

fn area(dimensions: (u32, u32)) -> u32 {
    dimensions.0 * dimensions.1
}

リスト5-9: タプルで長方形の幅と高さを指定する

ある意味では、このプログラムはマシです。タプルのおかげで少し構造的になり、一引数を渡すだけになりました。 しかし別の意味では、このバージョンは明確性を失っています: タプルは要素に名前を付けないので、 計算が不明瞭になったのです。なぜなら、タプルの一部に添え字アクセスする必要があるからです。

面積計算で幅と高さを混在させるのなら問題はないのですが、長方形を画面に描画したいとなると、問題になるのです! タプルの添え字0で、添え字1高さであることを肝に銘じておかなければなりません。 他人がこのコードをいじることになったら、このことを割り出し、同様に肝に銘じなければならないでしょう。 容易く、このことを忘れたり、これらの値を混ぜこぜにしたりしてエラーを発生させてしまうでしょう。 データの意味をコードに載せていないからです。

構造体でリファクタリングする: より意味付けする

データのラベル付けで意味を付与するために構造体を使います。現在使用しているタプルを全体と一部に名前のあるデータ型に、 変形することができます。そう、リスト5-10に示したように。

ファイル名: src/main.rs

struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };

    println!(
        "The area of the rectangle is {} square pixels.",
        area(&rect1)
    );
}

fn area(rectangle: &Rectangle) -> u32 {
    rectangle.width * rectangle.height
}

リスト5-10: Rectangle構造体を定義する

ここでは、構造体を定義し、Rectangleという名前にしています。波括弧の中でwidthheightというフィールドを定義し、 u32という型にしました。それからmain内でRectangleの特定のインスタンスを生成し、 幅を30、高さを50にしました。

これでarea関数は引数が一つになり、この引数は名前がrectangle、型はRectangle構造体インスタンスへの不変借用になりました。 第4章で触れたように、構造体の所有権を奪うよりも借用する必要があります。こうすることでmainは所有権を保って、 rect1を使用し続けることができ、そのために関数シグニチャと関数呼び出し時に&を使っているわけです。

area関数は、Rectangleインスタンスのwidthheightフィールドにアクセスしています。 これで、areaの関数シグニチャは、我々の意図をズバリ示すようになりました: widthheightフィールドを使って、 Rectangleの面積を計算します。これにより、幅と高さが相互に関係していることが伝わり、 タプルの01という添え字を使うよりも、これらの値に説明的な名前を与えられるのです。プログラムの意図が明瞭になりました。

トレイトの導出で有用な機能を追加する

プログラムのデバッグをしている間に、Rectangleのインスタンスを出力し、フィールドの値を確認できると、 素晴らしいわけです。リスト5-11では、以前の章のように、println!マクロを試しに使用しようとしていますが、動きません。

ファイル名: src/main.rs

struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };

    // rect1は{}です
    println!("rect1 is {}", rect1);
}

リスト5-11: Rectangleのインスタンスを出力しようとする

このコードを走らせると、こんな感じのエラーが出ます:

error[E0277]: the trait bound `Rectangle: std::fmt::Display` is not satisfied
(エラー: トレイト境界`Rectangle: std::fmt::Display`が満たされていません)

println!マクロには、様々な整形があり、標準では、波括弧はDisplayとして知られる整形をするよう、 println!に指示するのです: 直接エンドユーザ向けの出力です。これまでに見てきた基本型は、 標準でDisplayを実装しています。というのも、1や他の基本型をユーザに見せる方法は一つしかないからです。 しかし構造体では、println!が出力を整形する方法は自明ではなくなります。出力方法がいくつもあるからです: カンマは必要なの?波かっこを出力する必要はある?全フィールドが見えるべき?この曖昧性のため、 Rustは必要なものを推測しようとせず、構造体にはDisplay実装が提供されないのです。

エラーを読み下すと、こんな有益な注意書きがあります:

`Rectangle` cannot be formatted with the default formatter; try using
`:?` instead if you are using a format string
(注釈: `Rectangle`は、デフォルト整形機では、整形できません; フォーマット文字列を使うのなら
代わりに`:?`を試してみてください)

試してみましょう!pritnln!マクロ呼び出しは、println!("rect1 is {:?}", rect1);という見た目になるでしょう。 波括弧内に:?という指定子を書くと、println!Debugと呼ばれる出力整形を使いたいと指示するのです。 Debugトレイトは、開発者にとって有用な方法で構造体を出力させてくれるので、 コードをデバッグしている最中に、値を確認することができます。

変更してコードを走らせてください。なに!まだエラーが出ます:

error[E0277]: the trait bound `Rectangle: std::fmt::Debug` is not satisfied
(エラー: トレイト境界`Rectangle: std::fmt::Debug`が満たされていません)

しかし今回も、コンパイラは有益な注意書きを残してくれています:

`Rectangle` cannot be formatted using `:?`; if it is defined in your
crate, add `#[derive(Debug)]` or manually implement it
(注釈: `Rectangle`は`:?`を使って整形できません; 自分のクレートで定義しているのなら
`#[derive(Debug)]`を追加するか、手動で実装してください)

確かにRustにはデバッグ用の情報を出力する機能が備わっていますが、この機能を構造体で使えるようにするには、 明示的な選択をしなければならないのです。そうするには、構造体定義の直前に#[derive(Debug)]という注釈を追加します。 そう、リスト5-12で示されている通りです。

ファイル名: src/main.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };

    println!("rect1 is {:?}", rect1);
}

リスト5-12: Debugトレイトを導出する注釈を追加し、 Rectangleインスタンスをデバッグ用整形機で出力する

これでプログラムを実行すれば、エラーは出ず、以下のような出力が得られるでしょう:

rect1 is Rectangle { width: 30, height: 50 }

素晴らしい!最善の出力ではないものの、このインスタンスの全フィールドの値を出力しているので、 デバッグ中には間違いなく役に立つでしょう。より大きな構造体があるなら、もう少し読みやすい出力の方が有用です; そのような場合には、println!文字列中の{:?}の代わりに{:#?}を使うことができます。 この例で{:#?}というスタイルを使用したら、出力は以下のようになるでしょう:

rect1 is Rectangle {
    width: 30,
    height: 50
}

Rustには、derive注釈で使えるトレイトが多く提供されており、独自の型に有用な振る舞いを追加することができます。 そのようなトレイトとその振る舞いは、付録Cで一覧になっています。 これらのトレイトを独自の動作とともに実装する方法だけでなく、独自のトレイトを生成する方法については、第10章で解説します。

area関数は、非常に特殊です: 長方形の面積を算出するだけです。Rectangle構造体とこの動作をより緊密に結び付けられると、 役に立つでしょう。なぜなら、他のどんな型でもうまく動作しなくなるからです。 area関数をRectangle型に定義されたareaメソッドに変形することで、 このコードをリファクタリングし続けられる方法について見ていきましょう。

関連キーワード:  Rectangle, プログラム, height, rect, 出力, width, 関数, エラー, println, 長方形