Rustの構造体(Struct):定義、インスタンス化、およびメソッドの実装
構造体(Struct)は、Rustでカスタムデータ型を定義するための中心的なツールであり、関連するデータを意味のある全体としてまとめるものです。
変数が「単一のデータ」であるとするなら、構造体は「関連するデータの集合」です。これは表のようなもので、各行がインスタンス、各列がフィールドに相当します。
1. 学習内容
structキーワードを使用して、名前付き構造体を定義します- 構造体のインスタンスを作成し、そのフィールドにアクセスする
implブロックを使用して、メソッドおよび関連関数を追加する- タプル構造体と単位構造体
- 構造フィールドの所有権と参照
- 構造体
..および#[derive(Debug)]を更新するための構文
2. 概念図
flowchart LR
subgraph "struct Definition"
DEF["struct User"]
F1["username: String"]
F2["email: String"]
F3["active: bool"]
F4["sign_in_count: u64"]
end
subgraph "Instantiation"
INS["let user = User { ... }"]
V1["username: 'alice'"]
V2["email: 'alice@example.com'"]
V3["active: true"]
V4["sign_in_count: 1"]
end
F1 -.->|"Field Mapping"| V1
F2 -.->|"Field Mapping"| V2
F3 -.->|"Field Mapping"| V3
F4 -.->|"Field Mapping"| V4
3. 学生管理システムの物語
(1) 課題:変動要因が散在する生徒への対応
マリアは、自分のクラスの生徒に関する情報を管理する必要がある教師です。各生徒には、以下の情報があります:
- 氏名、年齢、学年、GPA
彼女は最初、こう書いた:
let name1 = "Xiao Ming";
let age1 = 18;
let grade1 = "Senior Year";
let gpa1 = 3.8;
let name2 = "Xiao Hong";
let age2 = 17;
let grade2 = "11th Grade";
let gpa2 = 3.5;
学生数が40人に増えると、こうしたばらばらに分散した変数は完全に管理不能になります。成績証明書を印刷するには40行のコードを記述する必要があり、たった1つのフィールドを変更するだけでも、4か所を修正しなければなりません。
(2) Rustの構造体によるアプローチ
struct Student {
name: String,
age: u8,
grade: String,
gpa: f64,
}
fn main() {
let student1 = Student {
name: String::from("Xiao Ming"),
age: 18,
grade: String::from("Senior 3"),
gpa: 3.8,
};
let student2 = Student {
name: String::from("Xiao Hong"),
age: 17,
grade: String::from("Senior 2"),
gpa: 3.5,
};
println!("{}: {} years old, Grade: {}, GPA: {:.1}",
student1.name, student1.age, student1.grade, student1.gpa);
println!("{}: {} years old, Grade: {}, GPA: {:.1}",
student2.name, student2.age, student2.grade, student2.gpa);
}
structは「テンプレート」のようなもので、「学生」というオブジェクトがどのような情報を含むべきかを定義します。各インスタンスは、記入済みのフォームのようなものです。フィールドへのアクセスは
.を介して行われ、これは明確かつ安全です。
4. 構造の種類
graph TB
A[struct Type] --> B[Naming Structures: Fields have names and types]
A --> C[Tuple Structure: The field has a type but no name]
A --> D[Unit Structure: No fields]
B --> E[struct User { name: String, age: u8 }]
C --> F[struct Color(i32, i32, i32)]
D --> G[struct EmptyMarker]
| タイプ | 定義 | ユースケース |
|---|---|---|
| 命名規則 | struct S { f1: T, f2: T } |
ほとんどの場合、フィールド名はそれ自体が説明となっている |
| タプルの構造 | struct S(T1, T2) |
単一の概念(例:RGB色、座標)をカプセル化する |
| ユニットの構造 | struct S; |
型マーカー、特性の実装用プレースホルダー |
(2) 手法と関連関数
| 型 | 最初の引数 | 呼び出し規約 | 所有権 | 代表的な用途 |
|---|---|---|---|---|
| メソッド | &self |
obj.method() |
借用 | インスタンスデータの読み取り |
| メソッド | &mut self |
obj.method() |
変数の借用 | インスタンスデータの変更 |
| メソッド | self |
obj.method() |
消費量 | インスタンスの変換/破棄 |
| 関連関数 | なし | Type::function() |
— | コンストラクタ、ファクトリメソッド |
(3) よく使われる派生形質のクイックリファレンス
| 特性 | 機能 | 自動適用条件 | 例 |
|---|---|---|---|
Debug |
{:?} 書式付き印刷 |
すべてのフィールドをデバッグ | #[derive(Debug)] |
Clone |
.clone() ディープコピー |
すべてのフィールドを複製 | #[derive(Clone)] |
Copy |
割り当て時の自動コピー | クローン + すべてのフィールドのコピー | #[derive(Copy, Clone)] |
PartialEq |
== / != の比較 |
すべてのフィールドで PartialEq を実装 | #[derive(PartialEq)] |
Hash |
HashMapのキーとして使用 | すべてのフィールドがHashを実装 | #[derive(Hash)] |
Default |
デフォルト値 | すべてのフィールドにデフォルト値が設定されています | #[derive(Default)] |
5. 構造の例
(1) ▶ サンプル:名前付き構造体の完全な使い方(難易度 ⭐)
// ============================================
// Naming Structures: Definition, Instantiation, Field Access
// ============================================
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
// Create an Instance
let rect = Rectangle {
width: 30,
height: 50,
};
// Access Fields
println!("Width: {}, Height: {}", rect.width, rect.height);
// Variable Instances
let mut mutable_rect = Rectangle {
width: 10,
height: 20,
};
mutable_rect.width = 15; // ✅ Can be modified
println!("Width after modification: {}", mutable_rect.width);
// Use Debug Print
println!("rect: {:?}", rect);
println!("rect (pretty): {:#?}", rect);
}
出力:
Width: 30, Height: 50
Width after modification: 15
rect: Rectangle { width: 30, height: 50 }
rect (pretty): Rectangle {
width: 30,
height: 50,
}
#[derive(Debug)]構造体を{:?}または{:#?}の書式で出力できるようにします。これは、デバッグ中に最もよく使用される派生トレイトの一つです。
(2) ▶ サンプル:構造体の更新構文とフィールドの省略形(難易度 ⭐⭐)
// ============================================
// Initializing Field Abbreviations + Update Syntax
// ============================================
#[derive(Debug)]
struct User {
username: String,
email: String,
active: bool,
sign_in_count: u64,
}
fn build_user(username: String, email: String) -> User {
User {
username, // When field names and variable names are the same, they can be written in shorthand.
email, // Equivalent to email: email
active: true,
sign_in_count: 1,
}
}
fn main() {
let user1 = build_user(
String::from("alice"),
String::from("alice@example.com"),
);
// Update Syntax: Based on user1 create user2, modify only email and username
let user2 = User {
email: String::from("alice_new@example.com"),
username: String::from("alice_new"),
..user1 // Other fields copied from user1 (Note: String fields will move!)
};
// println!("user1: {}", user1.username); // ❌ username was moved to user2
println!("user2: {:?}", user2);
println!("user1.active: {}", user1.active); // ✅ bool is Copy, still valid
}
出力:
user2: User { username: "alice_new", email: "alice_new@example.com", active: true, sign_in_count: 1 }
user1.active: true
フィールドの省略形:変数名が構造体のフィールド名と一致する場合、
field: fieldのコロンは省略可能です。更新された構文..は、残りのフィールドを別のインスタンスからコピーします。なお、これは Clone ではなく移動セマンティクスを使用することに注意してください。
(3) ▶ サンプル:impl ブロック — 構造体にメソッドを追加する (難易度: ⭐⭐)
// ============================================
// impl block: Methods (&self) and associated functions (no &self)
// ============================================
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
// Methods: &self is a reference to a Rectangle instance
fn area(&self) -> u32 {
self.width * self.height
}
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
// Associative Functions (similar to static methods): no &self
fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
let rect2 = Rectangle {
width: 10,
height: 20,
};
let square = Rectangle::square(15); // Correlation functions are used to :: Call
println!("rect1 Area: {}", rect1.area()); // 1500
println!("rect1 Can it accommodate? rect2: {}", rect1.can_hold(&rect2)); // true
println!("Area of a Square: {}", square.area()); // 225
}
出力:
rect1 Area: 1500
rect1 Can it accommodate? rect2: true
Area of a Square: 225
メソッド(Method)の最初のパラメータは
&self(または&mut self)であり、.を通じて呼び出されます。関連関数(Associated Function)には&selfが存在せず、::を通じて呼び出されます。その典型的な例がString::from()です。
(4) ▶ サンプル:タプルの構造 (難易度 ⭐⭐)
// ============================================
// Tuple Structure: Suitable for "it's just a bunch of values" scenarios
// ============================================
#[derive(Debug)]
struct Color(i32, i32, i32); // RGB Color
#[derive(Debug)]
struct Point(i32, i32, i32); // 3D Coordinates
fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
// Fields Accessed via Indexes (tuple-like)
println!("Black: R={}, G={}, B={}", black.0, black.1, black.2);
println!("The Beginning: x={}, y={}, z={}", origin.0, origin.1, origin.2);
// Tuple structures are not the same as tuples!
// let wrong: Color = origin; // ❌ Compilation Error: Point cannot be assigned to Color
// Although their internal structures are the same, they are different types
// This prevents "mixing up colors and coordinates" bugs
}
出力:
Black: R=0, G=0, B=0
The Beginning: x=0, y=0, z=0
タプル構造体にはフィールド名がなく、型のみが指定されます。これはタプルと似ていますが、独自の型名を持っています。
Color(0,0,0)とPoint(0,0,0)は内部的には同一ですが、型が異なります。これにより、混同を防ぐことができます。
(5) ▶ サンプル:総合演習—学生管理システム(難易度 ⭐⭐⭐)
// ============================================
// Comprehensive Example: Structure-Based Approach, Associated Functions and Update Syntax
// ============================================
#[derive(Debug, Clone)]
struct Student {
name: String,
age: u8,
scores: Vec<u32>,
}
impl Student {
fn new(name: &str, age: u8) -> Self {
Student {
name: name.to_string(),
age,
scores: Vec::new(),
}
}
fn add_score(&mut self, score: u32) {
self.scores.push(score);
}
fn average(&self) -> f64 {
if self.scores.is_empty() {
return 0.0;
}
let sum: u32 = self.scores.iter().sum();
sum as f64 / self.scores.len() as f64
}
fn grade(&self) -> char {
match self.average() as u32 {
90..=100 => 'A',
80..=89 => 'B',
70..=79 => 'C',
60..=69 => 'D',
_ => 'F',
}
}
fn summary(&self) -> String {
format!("{} ({} years old) Average: {:.1} Level: {}", self.name, self.age, self.average(), self.grade())
}
}
fn main() {
let mut alice = Student::new("Alice", 20);
alice.add_score(95);
alice.add_score(88);
alice.add_score(92);
println!("{}", alice.summary());
let mut bob = Student::new("Bob", 21);
bob.add_score(72);
bob.add_score(65);
bob.add_score(58);
println!("{}", bob.summary());
let mut charlie = Student {
name: String::from("Charlie"),
..alice.clone()
};
charlie.name = String::from("Charlie");
charlie.add_score(100);
println!("{}", charlie.summary());
let students = [&alice, &bob, &charlie];
println!("\n=== Class Rankings ===");
let mut ranked: Vec<_> = students.iter().collect();
ranked.sort_by(|a, b| b.average().partial_cmp(&a.average()).unwrap());
for (i, s) in ranked.iter().enumerate() {
println!("#{}: {}", i + 1, s.summary());
}
}
出力:
Alice (20 years old) Average: 91.7 Level: A
Bob (21 years old) Average: 65.0 Level: D
Charlie (20 years old) Average: 93.8 Level: A
=== Class Rankings ===
#1: Charlie (20 years old) Average: 93.8 Level: A
#2: Alice (20 years old) Average: 91.7 Level: A
#3: Bob (21 years old) Average: 65.0 Level: D
この例では、関連付け関数
new()、メソッドadd_score()/average()/grade()、および更新構文..alice.clone()と#[derive(Debug, Clone)]を組み合わせて使用しています。構造体は、関連するデータやメソッドを整理するための中心的なツールです。
❓ よくある質問
&self 付き)とアソシエイト関数(&self なし)は、それぞれどのような場合に使用すべきですか?📖 まとめ
- 名前付き構造体は、フィールド名が明確であるため、最も一般的に使用される構造体のタイプです
- タプル構造は、「単なる値の集合」(RGB、座標など)を扱う場面に適しています。
- ユニット構造体にはフィールドがなく、型の注釈に使用されます。
impl構造体に メソッド (&self) および 関連関数(なし&self)を追加する- フィールド略称の初期化:変数名がフィールド名と一致する場合は、コロンを省略する
- 構文の更新
..other: インスタンスから残りのフィールドをコピーする #[derive(Debug)]構造体をプリント可能にする
📝 練習問題
- 難易度 ⭐:
title: String、author: String、year: u32を含む構造体Bookを定義してください。2つのインスタンスを作成し、タイトルと著者を表示してください。 - 難易度 ⭐⭐:
fn perimeter(&self) -> u32に、周囲長を計算するメソッドを追加してください。また、幅と高さが等しい長方形を作成する関連関数fn from_width(w: u32) -> Rectangleを追加してください。 - 難易度 ⭐⭐⭐: 距離(メートルおよびセンチメートル単位)を表すタプル構造
Distance(f64, f64)を定義してください。メートル単位での合計値を返すメソッドfn to_meters(&self) -> f64を実装してください(例えば、Distance(1, 50)は1.50を返します)。



