Rust入門

【Rust入門】構造体の使い方を分かりやすく解説

【Rust入門】構造体の使い方を分かりやすく解説
naoki-hn

Rust における構造体(structの使い方の基本について初心者にも分かりやすく解説します。

Rust の構造体(struct

構造体(struct)とは

Rust では、複数の値を1つにまとめて扱う構造体(structを使います。これは、オブジェクト指向言語におけるクラスのような役割をします。

ただし、Rust にはクラスの構文は存在しません。Rust は「安全性と柔軟性の両立」を重視したプログラミング言語であり、継承を使わずに構造体(struct)と実装(impl)、トレイト(trait)を組み合わせることでクラスのような柔軟な振る舞いを実現しています。

この記事では、Rust の構造体の基本的な使い方から便利な初期化方法、メソッド定義、所有権の考え方まで初心者にも分かりやすく解説します。

構造体の定義方法とインスタンス化

構造体の定義方法

Rust の構造体は、自分で定義する「型」の一種です。複数の異なるデータ型の値を1つにまとめることができるデータ型として扱うことができます。例えば、人物の情報を表現する場合には以下のように Person 構造体を定義できます。

// Person構造体を定義
struct Person {
    first_name: String,
    last_name: String,
    sex: String,
    age: u32,
    birthday: String,
}

fn main() {
    // Personをインスタンスを作成する
    let person = Person {
        first_name: String::from("太郎"),
        last_name: String::from("山田"),
        sex: String::from("男性"),
        age: 25,
        birthday: String::from("2000-01-01"),
    };

    println!(
        "{}{}さん({})は、{}歳で誕生日は{}です。", 
        person.last_name, person.first_name, person.sex, person.age, person.birthday
    );
}
【実行結果】
山田太郎さん(男性)は、25歳で誕生日は2000-01-01です。

構造体は struct キーワードを使って構造体名を定義します。Rust では構造体名には UpperCamelCase という各単語の頭文字を大文字にして単語をつなげる形式が慣習です。

構造体が持つ各要素(first_name など)のことをフィールドと言いますが、フィールドは「フィールド名: 型」という形で定義します。構造体のインスタンスを作成する際には、let person のところで定義しているように「フィールド名: 値」という形で列挙します。

構造体の各要素にアクセスしたい場合には、person.first_name のように「インスタンス名.フィールド名」でアクセスすることが可能です。

構造体は全体が「mutable」または「immutable」

Rust では、変数は基本的に不変(immutable)です。これは、Rust の安全性を高める仕組みで値を変更する変数は、プログラマが明示的に mut キーワードで明示する必要があります。

Rust の構造体では、構造体全体が「可変(mutable)」か「不変(immutable)」かを指定します。フィールド単位ではなく、構造体単位で可変性が決まる点に注意しましょう。

fn main() {
    // 可変 (mutable) で変数を定義
    let mut person1 = Person {
        first_name: String::from("太郎"),
        last_name: String::from("山田"),
        sex: String::from("男性"),
        age: 25,
        birthday: String::from("2000-01-01"),
    };
    
    // 値の変更はOK
    person1.age += 1;

    // 不変 (immutable) で変数を定義
    let person2 = Person {
        first_name: String::from("太郎"),
        last_name: String::from("山田"),
        sex: String::from("男性"),
        age: 25,
        birthday: String::from("2000-01-01"),
    };

    // 値の変更はできない (コンパイルエラー)
    // person2.age += 1;
}

上記の例で、person2 は immutable なので、person2.age += 1; の行のコメントアウトをはずすとコンパイルエラーとなります。Rust では、このように構造体全体で可変性を統一することで一貫性を保証できるようにしています。

以下についても参考にしてください。

【Rust入門】変数と定数を分かりやすく解説

様々な初期化方法

構造体を定義し、インスタンスを初期化する際には便利な初期化方法があるので紹介します。

フィールドと変数が同名のとき(フィールド初期化省略記法)

関数 create_person に変数を渡して構造体をインスタンス化し、返却する場合を考えてみます。この時、通常は以下のように記載します。

fn create_person(
    first_name: String,
    last_name: String,
    sex: String,
    age: u32,
    birthday: String,
) -> Person {
    // Person インスタンスを生成して返却する
    Person {
        first_name: first_name,
        last_name: last_name,
        sex: sex,
        age: age,
        birthday: birthday,
    }
}

この例では「first_name: first_name」のように毎回同じ記載をするのが面倒です。このようにフィールド名と変数名が同じ場合のために Rust では「フィールド初期化省略記法」というものが用意されており「フィールド名: 変数名」の部分を以下のように省略できます。

fn create_person(
    first_name: String,
    last_name: String,
    sex: String,
    age: u32,
    birthday: String,
) -> Person {
    // フィールド初期化省略記法
    Person {
        first_name,
        last_name,
        sex,
        age,
        birthday,
    }
}

他のインスタンスからインスタンスを生成(構造体更新記法)

同様のフィールド値がある場合に毎回構造体の定義を書くのは大変です。そのような際に、既存の構造体インスタンスをもとに、新しいインスタンスを作成する構造体更新記法があります。構造体更新記法では「..」を使用して以下のように定義できます。

fn main() {
    let person1 = Person {
        first_name: String::from("太郎"),
        last_name: String::from("山田"),
        sex: String::from("男性"),
        age: 25,
        birthday: String::from("2000-01-01"),
    };

    // 構造体更新記法で新しいインスタンスを生成する
    let person2 = Person {
        first_name: String::from("花子"),
        sex: String::from("女性"),
        ..person1
    };

    println!(
        "{}{}さん({})は、{}歳で誕生日は{}です。", 
        person2.last_name, person2.first_name, person2.sex, person2.age, person2.birthday
    );
}
【実行結果】
山田花子さん(女性)は、25歳で誕生日は2000-01-01です。
構造体更新記法で注意が必要なこと

構造体更新記法を紹介しましたが、実はこの方法は注意すべき点があります。それは、省略したフィールドのうち Copy トレイトが実装されていない型の場合は、所有権の移動が起こってしまうということです。上記で person2 を作成した後、以下のコードを書いてみてください。

    // Copy トレイトが実装されていないフィールドは移動してしまうので使えなくなる。
    // 基本型の age はコピーされるので使える
    // last_name と birthday は String 型なので所有権が移動し、使えなくなる。
    println!("{}", person1.first_name);
    println!("{}", person1.last_name);
    println!("{}", person1.sex);
    println!("{}", person1.age);
    println!("{}", person1.birthday);

person1 のフィールドのうち、last_namebirthday はアクセスできなくなっており、コンパイルエラーとなります。これは、構造体更新記法により所有権が移動しているためです。一方で、ageu32 型の基本型で Copy トレイトが実装されているため person2 のフィールドにコピーされ、person1 でも続いてアクセスができます。

このように StringVec のように所有権が移動する型をフィールドに含むような構造体の場合には注意が必要です。一方で、以下の Rectangle 型の例のように基本型ばかりを含むような型の場合は、全てコピーになるので安心して使用できます。

// 矩形構造体 Rectangle
struct Rectangle {
    width: u32,
    height: u32,
    color_code: u8,
}

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

    // 構造体更新記法で新しいインスタンスを生成する
    // 基本型ばかりなのでコピーになる
    let rect2 = Rectangle {
        color_code: 2,
        ..rect1
    };

    // rect1 のフィールドにもアクセスできる
    println!("矩形1 widhth:{}, height:{}, color_code:{}", rect1.width, rect1.height, rect1.color_code);
    println!("矩形2 widhth:{}, height:{}, color_code:{}", rect2.width, rect2.height, rect2.color_code);
}
【実行結果】
矩形1 widhth:100, height:50, color_code:1
矩形2 widhth:100, height:50, color_code:2

タプル構造体(名前付きでフィールドのない構造体)

タプル構造体とは、フィールド名を持たない構造体で、シンプルなデータ構造の定義に適しています。各フィールドの意味が明確で、名前を付ける必要がない、もしくは名前付けすることで冗長になってしまうようなケースでよく使用されます。よく例で出てくる ColorPoint を使って見てみましょう。

// Color タプル構造体
struct Color(u8, u8, u8);
// Point タプル構造体
struct Point(i32, i32);

fn main() {
    let black = Color(0, 0, 0);
    let origin = Point(0, 0);

    println!("黒 = ({}, {}, {})", black.0, black.1, black.2);
    println!("原点 = ({}, {})", origin.0, origin.1);
}
【実行結果】
黒 = (0, 0, 0)
原点 = (0, 0)

このように ColorPoint は、RGB や X軸, Y軸 のように順序と意味が慣例で明確なのでタプル構造体で定義することに適しています。なお、通常の構造体定義では {} であったのが、タプル構造体では () になっているので注意しましょう。

構造体のメソッド

Rust では、構造体に関連付けられた関数を「メソッド(method)」と呼びます。これは impl ブロックの中で定義され、self を引数に取ることが特徴です。

impl Person {
    // あいさつをするメソッド 
    fn greet(&self) {
        println!(
            "こんにちは。{}{}と言います。{}歳の{}で、誕生日は{}です。", 
            self.last_name, self.first_name, self.age, self.sex, self.birthday,
        );
    }

    // 氏名を返却するメソッド
    fn full_name(&self) -> String {
        format!("{}{}", self.last_name, self.first_name)
    }

    // 年齢を比較するメソッド (self以外の引数をとる場合)
    fn is_older_than(&self, other_age: u32) -> bool {
        self.age > other_age
    }

    // 誕生日で年齢を+1するメソッド (可変参照とする場合)
    fn have_birthday(&mut self) {
        self.age += 1;
    }

    // 新しいPersonインスタンスを生成する (関連関数という)
    fn new(
        first_name: &str,
        last_name: &str,
        sex: &str,
        age: u32,
        birthday: &str,
    ) -> Person {
        Person {
            first_name: first_name.to_string(),
            last_name: last_name.to_string(),
            sex: sex.to_string(),
            age,
            birthday: birthday.to_string(),
        }
    }
}

fn main() {
    // 関連関数を使って Person のインスタンスを作成
    let mut person = Person::new("太郎", "山田", "男性", 25, "2000-01-01");

    // greet メソッドの呼び出し
    person.greet();
    
    // full_name メソッドの呼び出し
    let full_name = person.full_name();
    println!("フルネーム: {}", full_name);
    
    // is_older_than メソッドの呼び出し
    let other_age = 30;
    if person.is_older_than(other_age) {
        println!("{}歳より年上です。", other_age);
    } else {
        println!("{}歳より年下です。", other_age);
    }

    // 誕生日を迎えたので have_birthday を呼び出し
    person.have_birthday();
    println!("誕生日を迎えて、{}歳になりました。", person.age);
}
【実行結果】
こんにちは。山田太郎と言います。25歳の男性で、誕生日は2000-01-01です。
フルネーム: 山田太郎
30歳より年下です。
誕生日を迎えて、26歳になりました。

メソッドでは、第1引数に self という自身を指す変数を取る必要があります。値を参照するのみの場合は、不変参照(&self)を引数に取れば十分ですが、構造体のフィールドを書き換えるような場合には、可変参照(&mut self)を受け取る必要があります。

また、通常の関数と同様に値の返却(full_name メソッド)や他の引数を受け取る(is_older_than メソッド)ということもできます。

なお、self を引数に取らない new のような関数を関連関数と言います。関連関数は、構造体のインスタンスを必要とせずに呼び出せる関数で、self を引数に取らないため static な性質を持ちます。主に new 関数のようなコンストラクタ的用途や、定数の返却、型変換などのユーティリティ関数に使われます。他のオブジェクト指向言語におけるクラスの static メソッドに相当します。

構造体の所有権

Rustでは、構造体全体が1つの所有権を持ち、変数間の代入によって所有権全体が移動します。

fn main() {
    let person1 = Person::new("太郎", "山田", "男性", 25, "2000-01-01");

    // 所有権が移動する
    let person2 = person1;

    // 以下はコメントアウトを外すとコンパイルエラー
    // println!("person1: {}", person1.first_name);

    // 所有権を移動させない場合は、参照を使用する
    let person3 = &person2;
    println!("person2: {}", person2.first_name);
    println!("person3: {}", person3.first_name);
}

上記の例では、let person2 = person1; の部分で所有権が移動するので、person1 はフィールド値にもうアクセスできません。

所有権を移動させない場合は、借用(不変参照 または 可変参照)を使用しますが、その時には構造体のフィールド全体が一貫した参照として扱われます。

ただし、個々のフィールドに対して移動や借用することも可能であり、その場合は、各フィールドで所有権の扱いと借用のルールが適用されます。上記の構造体更新記法で一部の String の所有権が移動したような例もそのような例の1つです。

Rust の所有権や借用の基本は以下を参考にしてください。

【Rust入門】所有権と借用の基本を分かりやすく解説

まとめ

この記事では、Rust における構造体(structの基本的な使い方について解説しました。

構造体は、複数の値をまとめて扱うデータ型として、Rust でのデータ構造の設計において非常に重要な役割を果たします。フィールドの初期化、省略記法や構造体更新記法、タプル構造体、メソッドや関連関数の定義方法などを学ぶことで、より柔軟で表現力のあるプログラムが書けるようになります。

また、Rust の特徴である所有権と借用の仕組みも構造体に密接に関係しており、安全なメモリ管理を実現するために正しく理解しておくことが重要です。ぜひ、実際にコードを書いて試しながら、構造体の理解を深めてみてください。

ABOUT ME
ホッシー
ホッシー
システムエンジニア
はじめまして。当サイトをご覧いただきありがとうございます。
私は製造業のメーカーで、DX推進や業務システムの設計・開発・導入を担当しているシステムエンジニアです。これまでに転職も経験しており、以前は大手電機メーカーでシステム開発に携わっていました。

これまでの業務を通じてさまざまなプログラミング言語や技術に触れてきましたが、その中でもRustの設計思想に惹かれ、この言語についてもっと深く学びたい、そしてその魅力を発信していきたいと思い、このサイトを立ち上げました。

自身の学びを整理しつつ、同じようにRustに興味を持つ方のお役に立てるような情報を発信していければと思っています。どうぞよろしくお願いいたします。

※キャラクターデザイン:ゼイルン様
記事URLをコピーしました