構造体を使ったサンプルプログラム

RustRustBeginner
オンラインで実践に進む

This tutorial is from open-source community. Access the source code

💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください

はじめに

構造体を使ったサンプルプログラムへようこそ。この実験は、Rust Bookの一部です。LabEx で Rust のスキルを練習できます。

この実験では、構造体を使って長方形の面積を計算するプログラムを書き、幅と高さをそれぞれ別の変数で使っていた初期コードをリファクタリングします。


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/DataTypesGroup -.-> rust/integer_types("Integer Types") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") subgraph Lab Skills rust/variable_declarations -.-> lab-100396{{"構造体を使ったサンプルプログラム"}} rust/integer_types -.-> lab-100396{{"構造体を使ったサンプルプログラム"}} rust/function_syntax -.-> lab-100396{{"構造体を使ったサンプルプログラム"}} rust/expressions_statements -.-> lab-100396{{"構造体を使ったサンプルプログラム"}} end

構造体を使ったサンプルプログラム

構造体を使う場合があることを理解するために、長方形の面積を計算するプログラムを書いてみましょう。最初は単一の変数を使って始め、その後、構造体を使うようにプログラムをリファクタリングしていきます。

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]。波括弧の中で、フィールドをwidthheightとして定義し、どちらも型u32です [2]。そして、mainでは、幅が30、高さが50Rectangleの特定のインスタンスを作成しました [3]。

area関数は、今では 1 つのパラメータで定義されており、それをrectangleと名付けています。その型は、構造体Rectangleインスタンスの不変参照です [4]。第 4 章で述べたように、構造体の所有権を取得するのではなく、構造体を参照するようにします。このようにすることで、mainは所有権を保持し、rect1を継続して使用できます。これが、関数シグネチャと関数呼び出しの場所で&を使う理由です。

area関数は、Rectangleインスタンスのwidthheightフィールドにアクセスします [5] (借用した構造体インスタンスのフィールドにアクセスすると、フィールド値が移動しないことに注意してください。これが、構造体の借用をよく見る理由です)。areaの関数シグネチャは、今では私たちの意図を正確に表しています。Rectangleの面積を、そのwidthheightフィールドを使って計算します。これにより、幅と高さが互いに関連していることが伝わり、値に対して記述的な名前が与えられるようになります。タプルインデックス値の01を使うのではなくです。これは、明確さの面で勝利です。

派生トレイトを使った便利な機能の追加

プログラムのデバッグ中に、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)に出力されます。「標準出力ではなく標準エラーにエラーメッセージを書き込む」で、stderrstdoutについてもっと詳しく説明します。

次の例では、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 でさらに実験を行って、技術力を向上させましょう。