😸

#5 構造体【知識0がRustやってみる】

2024/04/10に公開

構造体を使用して関係のあるデータを構造化する

structまたは、構造体は、意味のあるグループを形成する複数の関連した値をまとめ、名前付けできる独自のデータ型です。 あなたがオブジェクト指向言語に造詣が深いなら、structはオブジェクトのデータ属性みたいなものです。 この章では、タプルと構造体を対照的に比較し、構造体の使用法をデモし、メソッドや関連関数を定義して、 構造体のデータに紐付く振る舞いを指定する方法について議論します。構造体とenum(第6章で議論します)は、 自分のプログラム領域で新しい型を定義し、Rustのコンパイル時型精査機能をフル活用する構成要素になります。

よろしくお願いします!

構造体を定義し、インスタンス化する

struct User { 
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
}

let user1 = User {
    username: String::from("someusername123"),
    email: String::from("someone@example.com"),
    sign_in_count: 1,
    active: true,
};

あらかじめ決まっているデータの名前と型を一つにまとめて名前を付けるイメージ
structで宣言し、中のデータ片をフィールドと言うらしい。

データベースもあらかじめテーブルを作って想定されたデータを入れるわけやから構造体はめちゃくちゃ合理的とおもう(小並)

let mut user1 = User {
    ...
}
user1.email = String::from("anotheremail@example.com");

↑値を変更するにはmutを宣言すればいけるが、emailだけじゃなくほかのフィールドも可変になるので頭の片隅に置いておく。
逆に、一部のフィールドのみ可変にはできない。

// ユーザーを初期化する関数
fn build_user(username: String, email: String) -> User {
    User {
        username, // フィールド名と引数名が同名であれば :username を省略できる。
        email: email, // 省略しない場合
        active: true,
        sign_in_count: 1,
    }
}
fn main(){
    //関数を使ったユーザー定義
    let user1 = build_user(nasubi, hoge@mail.com);

    // 構造体で直接定義する
    let user2 = User {
        username: String::from("anotherusername567"),
        email: String::from("another@example.com"),
        ..user1 // 既に定義してあるデータと同じフィールドは流用できる
    };
}

タプル構造体

今は使いどころがわからんけど、ColorはカラーコードでPointは3ゲームの得点?
決められた数だけ値を代入できる。

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);

構造体データの所有権

構造体にわざわざ所有権がのしかかるStringを使用したのは意図的であるとドヤりをきかせている様子

リスト5-1のUser構造体定義において、&str文字列スライス型ではなく、所有権のあるString型を使用しました。 これは意図的な選択です。というのも、この構造体のインスタンスには全データを所有してもらう必要があり、 このデータは、構造体全体が有効な間はずっと有効である必要があるのです。

構造体に、他の何かに所有されたデータへの参照を保持させることもできますが、 そうするにはライフタイムという第10章で議論するRustの機能を使用しなければなりません。 ライフタイムのおかげで構造体に参照されたデータが、構造体自体が有効な間、ずっと有効であることを保証してくれるのです。 ライフタイムを指定せずに構造体に参照を保持させようとしたとしましょう。以下の通りですが、これは動きません:

要するに、構造体のインスタンスを作る場合は全フィールドにしっかり値を束縛する必要があり、フィールドデータを定義した構造体インスタンス変数の中に借用(参照)データを含めてしまうと、メモリ開放などで参照している値を見失う可能性があり、それは良くないねってことやな。

参照を含めるにはライフタイムっていう新しい概念を使う必要があり、それを宣言せずに借用を含めるとコンパイルエラーになる。
ここではライフタイムの説明はない...きっと難しいんだろう

struct User {
    username: &str,
    email: &str,
    sign_in_count: u64,
    active: bool,
}
fn main() {
    let user1 = User {
        email: "someone@example.com",
        username: "someusername123",
        active: true,
        sign_in_count: 1,
    };
}

コンパイラは、ライフタイム指定子が必要だと怒るでしょう:

error[E0106]: missing lifetime specifier
(エラー: ライフタイム指定子がありません)
 -->
  | 
2 |     username: &str,
  |               ^ expected lifetime parameter
                   (ライフタイム引数を予期しました)

error[E0106]: missing lifetime specifier
 -->
  | 
3 |     email: &str,
  |            ^ expected lifetime parameter

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

まずはノー構造体のサンプルを見た後に、構造体を使っていくらしいぞ!
シンプルな掛け算プログラム

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
}
The area of the rectangle is 1500 square pixels.

さっそく構造体をつかっていこうぞ

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) // 参照で引数に渡して値を受け取る
        // これ以降でrect1を使用しないなら&を外して所有権を渡しても問題なし
    );
}
// 構造体を参照で受け取り掛け算して値を返す関数
fn area(rectangle: &Rectangle) -> u32 {
    rectangle.width * rectangle.height // 30×50 return 1500
}

rect1で構造体を初期化し掛け算をする関数へ参照を渡し出力するプログラムです。
この場合、関数から受け取った値をprintln!で出力しているのでOKですが、実は構造体インスタンスをそのまま出力はできません。

struct Rectangle {
    ...
}
fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };
    println!("rect1 is {}", rect1); // コンパイルエラー
}

他の変数やデータ型はコンパイラが型に合わせた出力を行ってくれていたが、構造体のフィールドにはさまざまな型を定義できるので、構造体の中身を全て確認してそれぞれの型を確認しそれに合わせた出力をするところまでまでは面倒見てくれないご様子。

なので構造体を出力したい場合はstructの上に#[derive(Debug)]を付け、println!("{:?}", hoge);とするとPHPのvardamp($hoge)のようにフィールドを出力してくれる。

インスタンスを直接出力すると{field_name: field_item}
struct_name.field_itemとするとfield_itemだけが出力される。

#[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 }
    // {:?} を使用するとプログラマ向けに要素を出力してくれる
}

メソッド記法

メソッドを定義する

メソッドは関数に似ています: fnキーワードと名前で宣言されるし、引数と返り値があるし、 どこか別の場所で呼び出された時に実行されるコードを含みます。ところが、 メソッドは構造体の文脈(あるいはenumかトレイトオブジェクトの。これらについては各々第6章と17章で解説します)で定義されるという点で、 関数とは異なり、最初の引数は必ずselfになり、これはメソッドが呼び出されている構造体インスタンスを表します。

これはそのまま他言語のクラスメソッドです。

インスタンス変数.メソッドで呼び出せる。
構造体インスタンスの値を触る場合は第一引数にselfを付ける。
触らなければselfは不要。

所有権も受け取りたいならself
参照だけ受け取るなら&self
インスタンスがミュータブルなら&mut self

構造体に関連する汎用メソッドを定義しておくことで関連付けしておくと後々管理が楽そうかな?

あまり意味はないっぽいけど同じ構造体にimplを複数置くことができる。

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}
impl Rectangle {
    fn area(&self) -> u32 { // rect1の参照を受け取り掛け算し値を返す
        self.width * self.height
    }
}
fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };
    println!(
        "The area of the rectangle is {} square pixels.",
        rect1.area()
    );
}

まとめ

構造体とimplめちゃくちゃ使い勝手よさそう。
コンパイラは親切やしrust書いててなんか気持ちいいんだよね()
手入れレイヤーとか全く経験なくてもrust書きたいってだけで学んでいいよね。いいよね..?

何か業務で使えるといいんだけどなあ
次回はEnumとパターンマッチング。

参考

Rust Book

Discussion