構造体を使ったプログラム例
構造体を使用したくなる可能性のあるケースを理解するために、長方形の面積を求めるプログラムを書きましょう。 単一の変数から始め、代わりに構造体を使うようにプログラムをリファクタリングします。
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 }
では、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 }
ある意味では、このプログラムはマシです。タプルのおかげで少し構造的になり、一引数を渡すだけになりました。 しかし別の意味では、このバージョンは明確性を失っています: タプルは要素に名前を付けないので、 計算が不明瞭になったのです。なぜなら、タプルの一部に添え字アクセスする必要があるからです。
面積計算で幅と高さを混在させるのなら問題はないのですが、長方形を画面に描画したいとなると、問題になるのです!
タプルの添え字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 }
ここでは、構造体を定義し、Rectangle
という名前にしています。波括弧の中でwidth
とheight
というフィールドを定義し、
u32
という型にしました。それからmain
内でRectangle
の特定のインスタンスを生成し、
幅を30、高さを50にしました。
これでarea
関数は引数が一つになり、この引数は名前がrectangle
、型はRectangle
構造体インスタンスへの不変借用になりました。
第4章で触れたように、構造体の所有権を奪うよりも借用する必要があります。こうすることでmain
は所有権を保って、
rect1
を使用し続けることができ、そのために関数シグニチャと関数呼び出し時に&
を使っているわけです。
area
関数は、Rectangle
インスタンスのwidth
とheight
フィールドにアクセスしています。
これで、area
の関数シグニチャは、我々の意図をズバリ示すようになりました: width
とheight
フィールドを使って、
Rectangle
の面積を計算します。これにより、幅と高さが相互に関係していることが伝わり、
タプルの0
や1
という添え字を使うよりも、これらの値に説明的な名前を与えられるのです。プログラムの意図が明瞭になりました。
トレイトの導出で有用な機能を追加する
プログラムのデバッグをしている間に、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);
}
このコードを走らせると、こんな感じのエラーが出ます:
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); }
これでプログラムを実行すれば、エラーは出ず、以下のような出力が得られるでしょう:
rect1 is Rectangle { width: 30, height: 50 }
素晴らしい!最善の出力ではないものの、このインスタンスの全フィールドの値を出力しているので、
デバッグ中には間違いなく役に立つでしょう。より大きな構造体があるなら、もう少し読みやすい出力の方が有用です;
そのような場合には、println!
文字列中の{:?}
の代わりに{:#?}
を使うことができます。
この例で{:#?}
というスタイルを使用したら、出力は以下のようになるでしょう:
rect1 is Rectangle {
width: 30,
height: 50
}
Rustには、derive
注釈で使えるトレイトが多く提供されており、独自の型に有用な振る舞いを追加することができます。
そのようなトレイトとその振る舞いは、付録Cで一覧になっています。
これらのトレイトを独自の動作とともに実装する方法だけでなく、独自のトレイトを生成する方法については、第10章で解説します。
area
関数は、非常に特殊です: 長方形の面積を算出するだけです。Rectangle
構造体とこの動作をより緊密に結び付けられると、
役に立つでしょう。なぜなら、他のどんな型でもうまく動作しなくなるからです。
area
関数をRectangle
型に定義されたarea
メソッドに変形することで、
このコードをリファクタリングし続けられる方法について見ていきましょう。