はじめに
構造体を使ったサンプルプログラムへようこそ。この実験は、Rust Bookの一部です。LabEx で Rust のスキルを練習できます。
この実験では、構造体を使って長方形の面積を計算するプログラムを書き、幅と高さをそれぞれ別の変数で使っていた初期コードをリファクタリングします。
This tutorial is from open-source community. Access the source code
💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください
構造体を使ったサンプルプログラムへようこそ。この実験は、Rust Bookの一部です。LabEx で Rust のスキルを練習できます。
この実験では、構造体を使って長方形の面積を計算するプログラムを書き、幅と高さをそれぞれ別の変数で使っていた初期コードをリファクタリングします。
構造体を使う場合があることを理解するために、長方形の面積を計算するプログラムを書いてみましょう。最初は単一の変数を使って始め、その後、構造体を使うようにプログラムをリファクタリングしていきます。
Cargo を使って、ピクセル単位で指定された長方形の幅と高さを受け取り、その長方形の面積を計算する新しいバイナリプロジェクト「rectangles」を作成しましょう。リスト 5-8 は、プロジェクトのsrc/main.rs
におけるそのような方法の 1 つを示す短いプログラムです。
ファイル名: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.
このコードは、各寸法をarea
関数に渡すことで長方形の面積を算出することに成功していますが、このコードをもっと明確で読みやすくするためにはもっと何かをすることができます。
このコードの問題は、area
のシグネチャに明らかです。
fn area(width: u32, height: u32) -> u32 {
area
関数は 1 つの長方形の面積を計算するはずですが、書いた関数は 2 つのパラメータを持ち、プログラム内のどこにもパラメータが関連していることが明確ではありません。幅と高さをまとめることで、もっと読みやすく管理しやすくなります。すでに「タプル型」でその方法の 1 つについて議論しています:タプルを使うことでです。
リスト 5-9 は、タプルを使ったプログラムの別のバージョンを示しています。
ファイル名:src/main.rs
fn main() {
let rect1 = (30, 50);
println!(
"The area of the rectangle is {} square pixels.",
1 area(rect1)
);
}
fn area(dimensions: (u32, u32)) -> u32 {
2 dimensions.0 * dimensions.1
}
リスト 5-9: タプルで長方形の幅と高さを指定する
ある意味では、このプログラムはより良いものになっています。タプルを使うことで少し構造を追加でき、今では 1 つの引数だけを渡しています [1]。しかし、別の面では、このバージョンはより明確ではありません。タプルは要素に名前を付けていないので、タプルの部分にインデックスを付けなければならず [2]、計算が明確になりません。
幅と高さを入れ替えても面積の計算には問題はありませんが、画面上に長方形を描画したい場合、それは重要になります!幅がタプルインデックス0
で、高さがタプルインデックス1
であることを覚えておかなければなりません。これは、誰かがコードを使う場合、理解して覚えるのがもっと難しくなります。コードの中でデータの意味を伝えていないので、エラーを起こしやすくなっています。
データにラベルを付けることで、構造体を使って意味を追加します。全体に名前を付け、各部分にも名前を付けることで、使っているタプルを構造体に変換できます。これはリスト 5-10 に示されています。
ファイル名:src/main.rs
1 struct Rectangle {
2 width: u32,
height: u32,
}
fn main() {
3 let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
area(&rect1)
);
}
4 fn area(rectangle: &Rectangle) -> u32 {
5 rectangle.width * rectangle.height
}
リスト 5-10: Rectangle
構造体を定義する
ここでは、構造体を定義してRectangle
と名付けました [1]。波括弧の中で、フィールドをwidth
とheight
として定義し、どちらも型u32
です [2]。そして、main
では、幅が30
、高さが50
のRectangle
の特定のインスタンスを作成しました [3]。
area
関数は、今では 1 つのパラメータで定義されており、それをrectangle
と名付けています。その型は、構造体Rectangle
インスタンスの不変参照です [4]。第 4 章で述べたように、構造体の所有権を取得するのではなく、構造体を参照するようにします。このようにすることで、main
は所有権を保持し、rect1
を継続して使用できます。これが、関数シグネチャと関数呼び出しの場所で&
を使う理由です。
area
関数は、Rectangle
インスタンスのwidth
とheight
フィールドにアクセスします [5] (借用した構造体インスタンスのフィールドにアクセスすると、フィールド値が移動しないことに注意してください。これが、構造体の借用をよく見る理由です)。area
の関数シグネチャは、今では私たちの意図を正確に表しています。Rectangle
の面積を、そのwidth
とheight
フィールドを使って計算します。これにより、幅と高さが互いに関連していることが伝わり、値に対して記述的な名前が与えられるようになります。タプルインデックス値の0
と1
を使うのではなくです。これは、明確さの面で勝利です。
プログラムのデバッグ中に、Rectangle
のインスタンスを表示して、そのすべてのフィールドの値を確認できると便利でしょう。リスト 5-11 では、これまでの章で使ってきたprintln!
マクロを使ってみます。しかし、これは機能しません。
ファイル名:src/main.rs
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {}", rect1);
}
リスト 5-11: Rectangle
インスタンスを表示しようとする
このコードをコンパイルすると、次のようなエラーが表示されます。
error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`
println!
マクロは様々な種類のフォーマットを行うことができます。デフォルトでは、波括弧はprintln!
に対して、Display
と呼ばれるフォーマットを使用するように指示します。これは、直接エンドユーザに提供する出力です。これまで見てきた基本型は、デフォルトでDisplay
を実装しています。なぜなら、1
や他の基本型をユーザに表示する方法は 1 通りだからです。しかし、構造体の場合、println!
が出力をどのようにフォーマットすべきかは明確ではありません。なぜなら、表示の可能性が多いからです。カンマを使うかどうか?波括弧を表示するかどうか?すべてのフィールドを表示するか?この曖昧さのため、Rust は私たちが何を望んでいるかを推測しようとせず、構造体にはprintln!
と{}
プレースホルダとともに使用するためのDisplay
の実装は提供されていません。
エラーを続けて読むと、次の助けになるメッセージがあります。
= help: the trait `std::fmt::Display` is not implemented for `Rectangle`
= note: in format strings you may be able to use `{:?}` (or {:#?} for
pretty-print) instead
試してみましょう!このときのprintln!
マクロの呼び出しは、println!("rect1 is {:?}", rect1);
のようになります。波括弧の中に指定子:?
を入れることで、println!
に対して、Debug
と呼ばれる出力フォーマットを使用するように指示します。Debug
トレイトを使うことで、開発者にとって便利な方法で構造体を表示できます。これにより、コードのデバッグ中に構造体の値を確認できます。
この変更を加えてコードをコンパイルします。残念ながら、まだエラーが表示されます。
error[E0277]: `Rectangle` doesn't implement `Debug`
しかし、再びコンパイラは助けになるメッセージを表示します。
= help: the trait `Debug` is not implemented for `Rectangle`
= note: add `#[derive(Debug)]` or manually implement `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,
}
Debug
フォーマットを使って値を表示する別の方法は、dbg!
マクロを使うことです。これは、式の所有権を取得します(println!
は参照を取得するのに対して)。コード内のdbg!
マクロの呼び出し箇所のファイルと行番号とともに、その式の結果の値を表示し、その値の所有権を返します。
注:
dbg!
マクロを呼び出すと、println!
が標準出力コンソールストリーム(stdout
)に出力するのに対して、標準エラーコンソールストリーム(stderr
)に出力されます。「標準出力ではなく標準エラーにエラーメッセージを書き込む」で、stderr
とstdout
についてもっと詳しく説明します。
次の例では、width
フィールドに割り当てられる値と、rect1
の全体の構造体の値に興味があります。
ファイル名:src/main.rs
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let scale = 2;
let rect1 = Rectangle {
1 width: dbg!(30 * scale),
height: 50,
};
2 dbg!(&rect1);
}
式30 * scale
の周りにdbg!
を入れることができます [1]。dbg!
は式の値の所有権を返すので、width
フィールドはdbg!
を使わなかった場合と同じ値を取得します。dbg!
がrect1
の所有権を取得してほしくないので、次の呼び出しではrect1
の参照を使います [2]。この例の出力は次のようになります。
[src/main.rs:10] 30 * scale = 60
[src/main.rs:14] &rect1 = Rectangle {
width: 60,
height: 50,
}
最初の出力は [1] で、式30 * scale
をデバッグしているところから来ており、その結果の値は60
です(整数に対して実装されているDebug
フォーマットは、値のみを表示するものです)。 [2] のdbg!
呼び出しは、&rect1
の値を出力します。これはRectangle
構造体です。この出力は、Rectangle
型のきれいなDebug
フォーマットを使用しています。dbg!
マクロは、コードが何をしているかを理解する際に非常に役立ちます!
Debug
トレイトに加えて、Rust はderive
属性とともに使用するための多くのトレイトを提供しており、これらはカスタム型に便利な動作を追加することができます。これらのトレイトとその動作は付録 C に記載されています。第 10 章では、これらのトレイトをカスタムの動作で実装する方法と、独自のトレイトを作成する方法について説明します。また、derive
以外にも多くの属性があります。詳細については、https://doc.rust-lang.org/reference/attributes.htmlの Rust リファレンスの「属性」セクションを参照してください。
area
関数は非常に特定的です。長方形の面積のみを計算します。この動作をRectangle
構造体にもっと密接に関連付けると便利でしょう。なぜなら、他の型では機能しないからです。次に、area
関数をRectangle
型に定義されたarea
メソッドに変換することで、このコードをどのようにリファクタリングできるか見てみましょう。
おめでとうございます! 「構造体を使ったサンプルプログラム」の実験を完了しました。LabEx でさらに実験を行って、技術力を向上させましょう。