構造体を使うプログラムの例
構造体を使いたくなる場面を理解するために、長方形の面積を計算するプログラムを書いてみましょう。最初は単一の変数を使って始め、その後、最終的に構造体を使うようになるまでプログラムをリファクタリングしていきます。
Cargo で rectangles という新しいバイナリプロジェクトを作成し、ピクセル単位で指定した長方形の幅と高さを受け取って、その長方形の面積を計算するようにしましょう。リスト 5-8 は、プロジェクトの 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 で実行してみましょう。
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.42s
Running `target/debug/rectangles`
The area of the rectangle is 1500 square pixels.
このコードは、各次元を指定して area 関数を呼び出すことで長方形の面積を正しく求めていますが、このコードをより明確で読みやすくするために、さらにできることがあります。
このコードの問題は、area のシグネチャを見ると明らかです。
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
}
area 関数は 1 つの長方形の面積を計算するはずですが、私たちが書いた関数には 2 つの引数があり、その引数どうしが関係していることはプログラムのどこを見ても明確ではありません。幅と高さをひとまとめにしたほうが、より読みやすく、扱いやすくなるでしょう。これを行う方法の 1 つについては、すでに第 3 章の 「タプル型」 の節で、タプルを使う方法として説明しました。
タプルでのリファクタリング
リスト 5-9 は、タプルを使ったこのプログラムの別バージョンを示しています。
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
}
ある意味では、このプログラムは改善されています。タプルによって少し構造を持たせることができ、今では引数を 1 つだけ渡せばよくなっています。しかし別の意味では、このバージョンはより不明瞭です。タプルの要素には名前がないため、タプルの各部分にインデックスでアクセスしなければならず、計算の意図が分かりにくくなっています。
面積の計算では幅と高さを取り違えても問題にはなりませんが、長方形を画面に描画したい場合には問題になります。その場合、width はタプルのインデックス 0、height はタプルのインデックス 1 であることを覚えておかなければなりません。ほかの人が私たちのコードを使う場合、それを理解して覚えておくのはさらに難しくなるでしょう。コードの中でデータの意味を伝えられていないため、今はエラーを入り込みやすくしてしまっています。
構造体でのリファクタリング
構造体は、データにラベルを付けることで意味を持たせるために使います。リスト 5-10 に示すように、使っているタプルを、全体に対する名前と各部分に対する名前を持つ構造体に変換できます。
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 では、幅 30、高さ 50 を持つ Rectangle の具体的なインスタンスを作成しました。
area 関数は今では 1 つの引数で定義されており、その名前を rectangle としています。この引数の型は、構造体 Rectangle のインスタンスへの不変借用です。第 4 章で述べたように、構造体の所有権を受け取るのではなく借用したいのです。そうすることで、main は所有権を保持したまま rect1 を引き続き使えます。これが、関数シグネチャと関数呼び出しで & を使っている理由です。
area 関数は Rectangle インスタンスの width フィールドと height フィールドにアクセスします(借用された構造体インスタンスのフィールドにアクセスしても、そのフィールドの値はムーブされないことに注意してください。そのため、構造体の借用をよく見かけます)。area の関数シグネチャは、今や私たちの意図を正確に表しています。つまり、Rectangle の width フィールドと height フィールドを使って、その面積を計算するということです。これにより、幅と高さが互いに関連していることが伝わり、タプルのインデックス値 0 や 1 を使う代わりに、値に説明的な名前を与えられます。これは明快さという点で大きな改善です。
derive したトレイトで機能を追加する
プログラムをデバッグしているときに Rectangle のインスタンスを出力して、すべてのフィールドの値を確認できると便利です。リスト 5-11 では、これまでの章で使ってきた println! マクロ を使おうとしています。しかし、これはうまくいきません。
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {rect1}");
}
このコードをコンパイルすると、中心となるメッセージとして次のようなエラーが出ます。
error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`
println! マクロはさまざまな種類の整形を行えますが、デフォルトでは波かっこは Display として知られる整形を使うよう println! に指示します。これは、エンドユーザーが直接読むことを意図した出力です。これまで見てきたプリミティブ型は、ユーザーに 1 やその他のプリミティブ型を見せる方法が実質 1 つしかないため、デフォルトで Display を実装しています。しかし構造体では、println! が出力をどう整形すべきかはそれほど明確ではありません。表示の仕方には複数の可能性があるからです。カンマは必要でしょうか。それとも不要でしょうか。波かっこも出力したいでしょうか。すべてのフィールドを表示すべきでしょうか。この曖昧さのため、Rust は私たちの意図を推測しようとはせず、構造体には println! と {} プレースホルダーで使える Display の実装が用意されていません。
エラーを読み進めると、次のような役立つ注記が見つかります。
| |`Rectangle` cannot be formatted with the default formatter
| required by this formatting parameter
試してみましょう! println! マクロ呼び出しは、今度は println!("rect1 is {rect1:?}"); のようになります。波かっこの中に指定子 :? を入れると、Debug と呼ばれる出力形式を使いたいことを println! に伝えます。Debug トレイトにより、開発者にとって役立つ形で構造体を出力できるようになり、コードをデバッグしている間にその値を確認できます。
この変更を加えた状態でコードをコンパイルしてください。しまった! まだエラーが出ます。
error[E0277]: `Rectangle` doesn't implement `Debug`
しかし今回も、コンパイラは役立つ注記を出してくれます。
| required by this formatting parameter
|
Rust には、デバッグ情報を出力する機能が 確かに 含まれていますが、その
機能をこの構造体で使えるようにするには、明示的に有効化しなければなりません。
そのために、リスト 5-12 に示すように、構造体定義の直前に外側属性
#[derive(Debug)] を追加します。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {rect1:?}");
}
これでプログラムを実行しても、エラーは発生せず、次のような出力が表示されます。
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/rectangles`
rect1 is Rectangle { width: 30, height: 50 }
いいですね! これは最も見栄えのよい出力ではありませんが、このインスタンスの
すべてのフィールドの値を表示してくれるので、デバッグ時には確実に役立ちます。
より大きな構造体では、もう少し読みやすい出力があると便利です。そのような場合
には、println! の文字列で {:?} の代わりに {:#?} を使えます。この例で
は、{:#?} スタイルを使うと、次のように出力されます。
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/rectangles`
rect1 is Rectangle {
width: 30,
height: 50,
}
Debug 形式を使って値を出力するもう 1 つの方法は、dbg!
macro を使うことです。これは式の所有権を受け取ります
(参照を受け取る println! とは対照的です)。そして、コード内でその dbg!
マクロ呼び出しがあるファイル名と行番号を、その式の結果の値とともに出力し、
その値の所有権を返します。
注:
dbg!マクロを呼び出すと、println!のように標準出力のコンソール ストリーム (stdout) に出力するのではなく、標準エラーのコンソール ストリーム (stderr) に出力されます。stderrとstdoutについては、 第 12 章の 「エラーを標準エラーにリダイレクトする」セクション でさらに詳しく説明します。
次は、width フィールドに代入される値と、rect1 に入っている構造体全体の
値の両方に関心がある例です。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let scale = 2;
let rect1 = Rectangle {
width: dbg!(30 * scale),
height: 50,
};
dbg!(&rect1);
}
式 30 * scale を dbg! で囲むことができます。dbg! は式の値の所有権を
返すので、width フィールドには、そこに dbg! 呼び出しがなかった場合と
同じ値が入ります。dbg! に rect1 の所有権を奪わせたくないので、次の呼び
出しでは rect1 への参照を使います。この例の出力は次のようになります。
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.61s
Running `target/debug/rectangles`
[src/main.rs:10:16] 30 * scale = 60
[src/main.rs:14:5] &rect1 = Rectangle {
width: 60,
height: 50,
}
最初の出力部分は、式 30 * scale をデバッグしている src/main.rs の 10 行目
から来ており、その結果の値は 60 です(整数に実装されている Debug
フォーマットは、その値だけを出力します)。src/main.rs の 14 行目にある
dbg! 呼び出しは、&rect1 の値を出力しており、これは Rectangle 構造体です。
この出力では、Rectangle 型の整形された Debug フォーマットが使われてい
ます。dbg! マクロは、コードが何をしているのかを突き止めようとしているとき
に、とても役立ちます!
Debug トレイトに加えて、Rust は derive 属性とともに使えるトレイトをいく
つも用意しており、カスタム型に便利な振る舞いを追加できます。それらのトレイト
とその振る舞いは 付録 C に一覧されています。第 10 章では、これらのトレイトにカスタムの振る
舞いを実装する方法と、独自のトレイトを作成する方法を説明します。derive
以外にも多くの属性があります。詳しくは、Rust Reference の「Attributes」
セクション を参照してください。
私たちの area 関数は非常に限定的です。長方形の面積しか計算しません。この
振る舞いを Rectangle 構造体にもっと密接に結び付けると便利でしょう。なぜな
ら、ほかのどの型に対しても動作しないからです。area 関数を、Rectangle 型
に定義された area メソッドに変えることで、このコードをさらにリファクタリン
グする方法を見ていきましょう。