Chapter 07

構造体

mebiusbox
mebiusbox
2023.03.16に更新

📌 構造体

構造体 はデータ型の要素を集めたものです.1つ1つの要素を フィールド と呼びます.構造体の定義は struct を使い,フィールドは名前と型を指定します.

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

構造体のオブジェクトを作成する場合は,各フィールドを key:value という形で束縛します.

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

可変のオブジェクトを作成するとすべてのフィールドが可変になります.オブジェクトのフィールドは . 演算子を使って指定します.

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

user1.email = String::from("anotheremail@example.com");

オブジェクト作成時に指定する変数名と構造体のフィールド名が一致している場合,フィールド名を省略できます.

fn build_user(email: String, username: String) -> User {
    User {
        email,
        username,
        active: true,
        sign_in_count: 1,
    }
}

あるオブジェクトのフィールドに束縛したものを使って,新しいオブジェクトを作成するときに便利な構文があります.オブジェクト作成時に,明示的にフィールドを指定しなかったものは .. の後に渡したオブジェクトのフィールドを束縛します.ただし,コピートレイトを実装している型なら複製され,そうでないなら所有権が移動することに注意が必要です.

let user2 = User {
    email: String::from("another@example.com"),
    username: String::from("anotherusername567"),
    ..user1
};

📌 タプル構造体

指定した要素で構成されたタプルに名前をつけられます.このようなタプルを タプル構造体 といいます.この場合,フィールド名はありません.タプル構造体は同じ構成をしていても別の型として区別されます.タプルは .0 というように要素のインデックスを指定するか,要素の分解を使います.

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

fn main() {
    let black = Color(0, 0, 0);
    let Point (x,y,z) = Point(0, 0, 0);
    
    println!("{} {} {}", black.0, black.1, black.2);
    println!("{} {} {}", x, y, z);
}

Rust は静的型付け言語です.この特性を利用して既存の型から新しい型を作成することで意図的な意味を付加させて制約できます.また,既存の型を利用するので薄いラッパー型と考えることもできます.これは Newtype パターンと呼ばれるもので,タプル構造体を利用する例の1つです.例えば, String 型から Password 型を作成し, {} で出力したときに伏せ字にする場合は次のようになります.

use std::fmt;

struct Password(String);

impl fmt::Display for Password {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0.chars().map(|_| '*').collect::<String>())
    }
}

fn main() {
    let a = String::from("123456789");
    println!("{}", a); // 123456789
    
    let a = Password(String::from("123456789"));
    println!("{}", a); // *********
}

📌 ユニット構造体

() のことをユニットと言いました.このようなフィールドを何も持たない構造体のことをユニット構造体(Unit-like Structs)と言います.ユニット構造体はフィールドを持たず,トレイトだけ実装するといった時に使われるようです.

📌 メソッド

メソッド は関数に似ていますが,構造体と関連していて, self を使うことで,そのメソッドを呼び出したオブジェクトを操作できます.メソッドの第一引数は必ず self になります.また,基本的に不変参照 (&self) か可変参照 (&mut self) になります.もちろん,可変参照のメソッドは,呼び出し元が可変の所有権を使って呼び出さなければなりません.メソッドは impl ブロックの中で,関数と同じく fn を使って定義します.

struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50, };

    println!("The area of the rectangle is {} square pixels.", rect1.area());
}

メソッドの呼び出し方法は2種類あります.1つは束縛したオブジェクトに.演算子をつけて呼び出す方法です(rect1.area()).もう1つはパス(::)を使って 型::メソッド名 で呼び出す方法です.

...

fn main() {
    let rect1 = Rectangle { width: 30, height: 50, };

    println!("The area of the rectangle is {} square pixels.", Rectangle::area(&rect1));
}

impl ブロックは複数定義できます.トレイトごとに実装を分けられます.

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

📌 関連関数

impl ブロックの中で関数を定義できます.それは self を引数に取りません.このような関数を 関連関数 と呼びます.これは構造体に関連しているにも関わらず,そのオブジェクトが無くても呼び出せます.関連関数は主にその構造体のオブジェクトを生成する関数の定義に使い,そのような関連関数には new という名前が使われます.関連関数はパス(::)を使って呼び出します.

struct Point {
    x: f64,
    y: f64,
}

impl Point {
    fn new(x: f64, y: f64) -> Self { // Self は実装している型の型エイリアス
        Self { x, y }
    }
}

fn main() {
    let a = Point::new(3., 5.);

    print!("x={}, y={}", a.x, a.y);
}