[Rustのはじめ方] Part16: 構造体(struct)と実装(impl)

Rust

関連するデータをまとめて、独自の型を作成しよう!

Rustプログラミングの世界へようこそ!😊 これまでのステップで、変数、データ型、制御構文、所有権といった基本的な概念を学んできました。今回は、より複雑なデータ構造を扱うための強力な機能、「構造体(struct)」とその振る舞いを定義する「実装(impl)」について学びます。

構造体を使うことで、関連する複数の値をひとまとめにして、意味のある独自のデータ型を作成できます。例えば、「ユーザー」を表す型を定義して、その中にID、名前、メールアドレスなどの情報を持たせることができます。

構造体(struct)の定義

構造体は struct キーワードを使って定義します。いくつかの形式があります。

フィールドを持つ構造体

最も一般的な形式で、各フィールドに名前と型を指定します。


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

// タプル構造体 (フィールド名がない)
struct Color(i32, i32, i32); // RGB値など
struct Point(i32, i32, i32); // 3次元座標など

// ユニット様構造体 (フィールドがない)
// トレイトを実装したいが、データは保持する必要がない場合に有用
struct AlwaysEqual;
        

User 構造体は、ユーザーの状態、名前、メールアドレス、サインイン回数という4つのフィールドを持っています。ColorPoint はタプル構造体と呼ばれ、フィールド名を持ちません。AlwaysEqual はユニット様構造体で、フィールドを全く持ちません。

構造体のインスタンス化

定義した構造体を使うには、その「インスタンス」を作成します。インスタンス化するには、構造体名を指定し、波括弧 {} の中に各フィールド名と値を key: value ペアで記述します。


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

fn main() {
    // User構造体のインスタンスを作成
    let mut user1 = User {
        email: String::from("someone@example.com"),
        username: String::from("someusername123"),
        active: true,
        sign_in_count: 1,
    };

    // フィールドの値にアクセス (ドット記法)
    println!("Email: {}", user1.email); // Output: Email: someone@example.com

    // フィールドの値を変更 (インスタンスが `mut` である必要あり)
    user1.email = String::from("anotheremail@example.com");
    println!("New Email: {}", user1.email); // Output: New Email: anotheremail@example.com

    // インスタンス生成関数 (後述の `impl` で定義) を使う例
    let user2 = build_user(String::from("user2@example.com"), String::from("user2"));
    println!("User2 Username: {}", user2.username);

    // 構造体更新構文 (`..`)
    // user1 の値を使って新しい User インスタンスを生成
    let user3 = User {
        email: String::from("user3@example.com"),
        ..user1 // user1 の残りのフィールド値をコピー (active, sign_in_count)
                // username は user1 の所有権が move されることに注意 (String のため)
                // active や sign_in_count は Copy トレイトを実装しているのでコピーされる
    };
    // println!("{}", user1.username); // エラー: username の所有権は user3 に move された

    println!("User3 Active: {}", user3.active);

    // タプル構造体のインスタンス化
    struct Color(i32, i32, i32);
    let black = Color(0, 0, 0);
    println!("Black color: R={}, G={}, B={}", black.0, black.1, black.2);

    // ユニット様構造体のインスタンス化
    struct AlwaysEqual;
    let subject = AlwaysEqual;
    // subject には特にアクセスできるデータはない
}

// Userインスタンスを生成するヘルパー関数
fn build_user(email: String, username: String) -> User {
    // フィールド名と変数名が同じ場合は省略可能 (フィールド初期化省略記法)
    User {
        email, // email: email, と同じ
        username, // username: username, と同じ
        active: true,
        sign_in_count: 1,
    }
}
        

💡 フィールド初期化省略記法

関数の引数名やローカル変数名が、構造体のフィールド名と同じ場合、field: variable の代わりに単に field と書くことができます。これはコードを簡潔にするのに役立ちます。

⚠️ 所有権と構造体更新構文

構造体更新構文 .. を使う際、コピーされるフィールドの型に注意が必要です。String のような所有権を持つ型の場合、値はムーブ(移動)されます。bool や数値型のように Copy トレイトを実装している型の場合、値はコピーされます。ムーブが発生すると、元のインスタンスのそのフィールドにはアクセスできなくなります。

メソッドの定義(impl)

構造体を定義した後、その構造体に関連する関数を定義したくなることがよくあります。例えば、長方形を表す構造体に対して、その面積を計算する関数などです。このような関数を「メソッド」と呼び、impl(implementation、実装)ブロックを使って定義します。


#[derive(Debug)] // デバッグ出力 {:?} を有効にする
struct Rectangle {
    width: u32,
    height: u32,
}

// Rectangle 構造体のための impl ブロック
impl Rectangle {
    // メソッド: 第一引数が `self`, `&self`, または `&mut self`
    // `&self` はインスタンスの不変の参照を受け取る (所有権は奪わない)
    fn area(&self) -> u32 {
        self.width * self.height
    }

    // 別の Rectangle インスタンスを受け取るメソッド
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }

    // 関連関数 (Associated Function): 第一引数が `self` ではない
    // よくコンストラクタとして使われる (インスタンスを生成する関数)
    // `Self` は `impl` している型 (この場合は Rectangle) を指すエイリアス
    fn square(size: u32) -> Self { // Self == Rectangle
        Self { width: size, height: size }
    }

    // 可変の参照 `&mut self` を受け取るメソッド
    // インスタンスの状態を変更する場合に使う
    fn set_width(&mut self, width: u32) {
        self.width = width;
    }
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };
    let rect2 = Rectangle { width: 10, height: 40 };
    let rect3 = Rectangle { width: 60, height: 45 };
    let mut rect4 = Rectangle { width: 10, height: 10 }; // 変更するので mut

    // メソッド呼び出し (`.` 演算子)
    println!(
        "The area of the rectangle {:?} is {} square pixels.",
        rect1,
        rect1.area() // rect1.area(&rect1) とほぼ同じだが、自動参照/脱参照が働く
    );

    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2)); // true
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3)); // false

    // 関連関数の呼び出し (`::` 演算子)
    let sq = Rectangle::square(25);
    println!("Created a square: {:?}", sq);

    // 可変メソッドの呼び出し
    println!("rect4 width before: {}", rect4.width);
    rect4.set_width(20); // rect4.set_width(&mut rect4) とほぼ同じ
    println!("rect4 width after: {}", rect4.width);
}
        

メソッドと関連関数

impl ブロック内で定義される関数には、大きく分けて2種類あります。

種類 第一引数 呼び出し方 説明
メソッド (Method) self, &self, または &mut self インスタンスに対して . 演算子 (例: instance.method()) インスタンスのデータにアクセスしたり、変更したりします。self は所有権を奪い、&self は不変の参照、&mut self は可変の参照を受け取ります。
関連関数 (Associated Function) self を取らない 型に対して :: 演算子 (例: StructName::function()) インスタンスを必要としない関数です。構造体の新しいインスタンスを返すコンストラクタ(初期化関数)としてよく使われます (例: String::from(), Rectangle::square())。

複数の impl ブロックを持つことも可能です。例えば、構造体の定義と基本的なメソッドを一つの impl ブロックに書き、特定の機能に関連するメソッド群を別の impl ブロックに分けることができます。


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

// 基本的な操作
impl Point {
    fn new(x: f64, y: f64) -> Self {
        Self { x, y }
    }
}

// 距離に関する操作
impl Point {
    fn distance_from_origin(&self) -> f64 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

fn main() {
    let p = Point::new(3.0, 4.0);
    println!("Distance from origin: {}", p.distance_from_origin()); // Output: 5
}
        

まとめ

今回は、Rustにおける構造体 struct とその実装 impl について学びました。✨

  • 構造体 (struct): 関連するデータをひとまとめにし、新しい型を定義する方法。フィールドを持つ構造体、タプル構造体、ユニット様構造体がある。
  • インスタンス化: 定義した構造体から具体的な値(インスタンス)を作成すること。フィールド初期化省略記法や構造体更新構文が便利。
  • 実装 (impl): 構造体に関連する関数(メソッドや関連関数)を定義するブロック。
  • メソッド: インスタンスを操作するための関数 (&self, &mut self を取る)。
  • 関連関数: 型自体に関連付けられた関数(コンストラクタなど)。

構造体と実装は、データを構造化し、そのデータに対する操作をカプセル化するための基本的な要素です。これらを使いこなすことで、より整理され、再利用性の高いコードを書くことができます。

次のステップでは、構造体と似ていますが、複数の「バリアント(種類)」を持つことができる「列挙型(enum)」について学びます。お楽しみに!🚀

コメント

タイトルとURLをコピーしました