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

Rust における構造体(struct)の使い方の基本について初心者にも分かりやすく解説します。
Rust の構造体(struct)
構造体(struct)とは
Rust では、複数の値を1つにまとめて扱う構造体(struct)が重要な要素です。これは、オブジェクト指向言語におけるクラスのような役割をします。
ただし、Rust にはクラス構文は存在しません。Rust は「安全性と柔軟性の両立」を重視したプログラミング言語であり、継承を使わずに構造体(struct)と実装(impl)、トレイト(trait)を組み合わせることでクラスのような柔軟な振る舞いを実現しています。
この記事では、Rust の構造体の基本的な使い方から便利な初期化方法、メソッド定義、所有権の考え方まで初心者にも分かりやすく解説します。
構造体の定義方法とインスタンス化
構造体の定義方法
Rust の構造体は、自分で定義する「型」の一種です。複数の異なるデータ型の値を1つにまとめることができるデータ型として扱うことができます。
例えば、人物の情報を表現する場合には以下のように Person 構造体を定義できます。
[person.rs]
// Person構造体を定義
#[derive(Debug)]
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
);
// Debugフォーマットで表示する
println!("{:?}", person);
}【実行結果】
山田太郎さん(男性)は、25歳で誕生日は2000-01-01です。
Person { first_name: "太郎", last_name: "山田", sex: "男性", age: 25, birthday: "2000-01-01" }構造体は struct キーワードを使って構造体名を定義します。Rust では構造体名には UpperCamelCase という各単語の頭文字を大文字にして単語をつなげる形式が慣習です。
構造体が持つ各要素(first_name など)のことをフィールドと言いますが、フィールドは「フィールド名: 型」という形で定義します。構造体のインスタンスを作成する際には、let person のところで定義しているように「フィールド名: 値」という形で列挙します。
構造体の各要素にアクセスしたい場合には、person.first_name のように「インスタンス名.フィールド名」でアクセスすることが可能です。
また、構造体の定義では [derive(Debug)] という記載をつけることで、Debug フォーマットでの表示ができます。println! マクロにおいて {:?} とすると、この部分に定義した構造体の各情報を自動でフォーマットして表示してくれます。[derive(Debug)] は、Debug トレイトを自動実装してくれる便利な機能ですので覚えておいてください。
構造体は全体が「mutable」または「immutable」
Rust では、変数は基本的に不変(immutable)です。これは、Rust の安全性を高める仕組みで値を変更する変数は、プログラマが明示的に mut キーワードで明示する必要があります。
Rust の構造体では、構造体全体が「可変(mutable)」か「不変(immutable)」かを指定します。フィールド単位ではなく、構造体単位で可変性が決まる点に注意しましょう。
[mutable_immutable.rs]
// Person 構造体の定義は同じなので省略
fn main() {
// 可変 (mutable) で変数を定義
let mut person1 = Person {
first_name: String::from("太郎"),
last_name: String::from("山田"),
sex: String::from("男性"),
age: 24,
birthday: String::from("2000-01-01"),
};
// 値の変更はOK
person1.age += 1;
println!(
"{} {} {} {} {}",
person1.last_name, person1.first_name, person1.sex, person1.age, person1.birthday
);
// 不変 (immutable) で変数を定義
let person2 = Person {
first_name: String::from("花子"),
last_name: String::from("山田"),
sex: String::from("女性"),
age: 20,
birthday: String::from("2005-01-01"),
};
println!(
"{} {} {} {} {}",
person2.last_name, person2.first_name, person2.sex, person2.age, person2.birthday
);
// 値の変更はできない (コンパイルエラー)
// person2.age += 1;
}
【実行結果】 山田 太郎 男性 25 2000-01-01 山田 花子 女性 20 2005-01-01
上記の例で、person2 は immutable なので、person2.age += 1; の行のコメントアウトを外すとコンパイルエラーとなります。
Rust では、このように構造体全体で可変性を統一することで一貫性を保証しています。
様々な初期化方法
構造体を定義し、インスタンスを初期化する際には便利な初期化方法があるので紹介します。
フィールドと変数が同名のとき(フィールド初期化省略記法)
関数 create_person に変数を渡して構造体をインスタンス化し、返却する場合を考えてみます。この時、通常は以下のように記載します。
[field_init.rs]
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 では「フィールド初期化省略記法」というものが用意されており「フィールド名: 変数名」の部分を以下のように省略できます。
[field_init_shorthand.rs]
fn create_person(
first_name: String,
last_name: String,
sex: String,
age: u32,
birthday: String,
) -> Person {
// フィールド初期化省略記法
Person {
first_name,
last_name,
sex,
age,
birthday,
}
}他のインスタンスからインスタンスを生成(構造体更新記法)
同様のフィールド値がある場合に毎回構造体の定義を書くのは大変です。そのような際に、既存の構造体インスタンスをもとに、新しいインスタンスを作成する構造体更新記法があります。構造体更新記法では「..」を使用して以下のように定義できます。
[update_syntax.rs]
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 を作成した後、以下のコードを書いてみてください。
[update_syntax_move.rs]
// 一部フィールドでも所有権の移動が起こると構造体本体を使用できない。
// ただし、個々のフィールドについては Copy トレイトの実装有無で挙動が異なる
// - Copy トレイトが実装されている場合: フィールドの値がコピーされるので使用できる
// - Copy トレイトが実装されていない場合: フィールドの所有権が移動するので使用不可
// 以下のコメントアウトを外すとコンパイルエラー
// println!("{:?}", person1);
println!("{}", person1.first_name);
// println!("{}", person1.last_name);
println!("{}", person1.sex);
println!("{}", person1.age);
// println!("{}", person1.birthday);person1 のフィールドのうち、last_name と birthday はアクセスできなくなっており、コンパイルエラーとなります。これは、構造体更新記法により所有権が移動しているためです。また、一部でもフィールドの所有権移動が起こると構造体本体の person1 へはアクセスできなくなります。
一方で、age は u32 型の基本型で Copy トレイトが実装されているため person2 のフィールドにコピーされるため、person1.age のように個別に指定すれば引き続きアクセスが可能です。
このように String や Vec のように所有権が移動する型をフィールドに含むような構造体の場合には構造体更新記法の使用は注意が必要です。例えば、以下の Rectangle 型の例のように基本型のみを含むような構造体の場合は、全てコピーになるので安心して使用できます。
[update_syntax_save.rs]
// 矩形構造体 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
タプル構造体(名前付きでフィールドのない構造体)
タプル構造体とは、フィールド名を持たない構造体で、シンプルなデータ構造の定義に適しています。各フィールドの意味が明確で、名前を付ける必要がない、もしくは名前付けすることで冗長になってしまうようなケースでよく使用されます。
よく例で紹介される Color や Point を使って見てみましょう。
[tuple_struct.rs]
// 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)
このように Color や Point は、RGB や X軸, Y軸 のように順序と意味が慣例で明確なのでタプル構造体で定義することに適しています。なお、通常の構造体定義では {} であったのが、タプル構造体では () になっているので注意しましょう。
構造体のメソッド
Rust では、構造体に関連付けられた関数を「メソッド(method)」と呼びます。これは impl ブロックの中で定義され、self を引数に取ることが特徴です。
[method.rs]
// Person 構造体の定義は同じなので省略
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つの所有権を持ち、変数間の代入によって所有権全体が移動します。
[ownership_borrowing.rs]
// Person 構造体の定義は同じなので省略
// メソッドの実装 (impl Person) は同じなので省略
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 における構造体(struct)の基本的な使い方について解説しました。
構造体は、複数の値をまとめて扱うデータ型として、Rust でのデータ構造の設計において非常に重要な役割を果たします。フィールドの初期化、省略記法や構造体更新記法、タプル構造体、メソッドや関連関数の定義方法などを学ぶことで、より柔軟で表現力のあるプログラムが書けるようになります。
また、Rust の特徴である所有権と借用の仕組みも構造体に密接に関係しており、安全なメモリ管理を実現するために正しく理解しておくことが重要です。ぜひ、実際にコードを書いて試しながら、構造体の理解を深めてみてください。
上記で紹介しているソースコードについては GitHub にて公開しています。参考にしていただければと思います。







