|
| 1 | +# 第6章:構造体とメソッド構文 |
| 2 | + |
| 3 | +他のオブジェクト指向言語(C++、Java、Pythonなど)の経験がある方にとって、Rustの**構造体(Struct)**は「クラス」に似ているように見えますが、決定的な違いがいくつかあります。 |
| 4 | + |
| 5 | +最も大きな違いは以下の2点です: |
| 6 | + |
| 7 | +1. **クラス継承が存在しない**: Rustには`extends`や親クラスという概念がありません(コードの再利用には、後述する「トレイト」や「コンポジション」を使用します)。 |
| 8 | +2. **データと振る舞いの分離**: データ定義(`struct`)とメソッド定義(`impl`)は明確に分かれています。 |
| 9 | + |
| 10 | +この章では、関連するデータをグループ化し、それに対する操作を定義する方法を学びます。 |
| 11 | + |
| 12 | +## 構造体の定義とインスタンス化 |
| 13 | + |
| 14 | +構造体は、異なる型の値を一つにまとめて名前を付けたカスタムデータ型です。 |
| 15 | + |
| 16 | +### 基本的な定義 |
| 17 | + |
| 18 | +C言語の `struct` や、メソッドを持たないクラスのようなものです。フィールド名と型を定義します。 |
| 19 | + |
| 20 | +```rust:user_struct.rs |
| 21 | +struct User { |
| 22 | + username: String, |
| 23 | + email: String, |
| 24 | + sign_in_count: u64, |
| 25 | + active: bool, |
| 26 | +} |
| 27 | + |
| 28 | +fn main() { |
| 29 | + // インスタンス化 |
| 30 | + // フィールドの順番は定義と異なっても構いません |
| 31 | + let mut user1 = User { |
| 32 | + email: String::from( "[email protected]"), |
| 33 | + username: String::from("someusername123"), |
| 34 | + active: true, |
| 35 | + sign_in_count: 1, |
| 36 | + }; |
| 37 | + |
| 38 | + // ドット記法でフィールドにアクセス |
| 39 | + user1.email = String::from( "[email protected]"); |
| 40 | + |
| 41 | + println!("User: {}, Email: {}", user1.username, user1.email); |
| 42 | +} |
| 43 | +``` |
| 44 | + |
| 45 | +```rust-exec:user_struct.rs |
| 46 | +User: someusername123, Email: another@example.com |
| 47 | +``` |
| 48 | + |
| 49 | +> **注意**: Rustでは、インスタンス全体が可変(`mut`)か不変かのどちらかになります。特定のフィールドだけを可変(`mut`)にすることはできません。 |
| 50 | +
|
| 51 | +### フィールド初期化省略記法 |
| 52 | + |
| 53 | +関数引数や変数の名前がフィールド名と同じ場合、記述を省略できます。これはJavaScriptのオブジェクト定義に似ています。 |
| 54 | + |
| 55 | +```rust |
| 56 | +fn build_user(email: String, username: String) -> User { |
| 57 | + User { |
| 58 | + email, // email: email と同じ |
| 59 | + username, // username: username と同じ |
| 60 | + active: true, |
| 61 | + sign_in_count: 1, |
| 62 | + } |
| 63 | +} |
| 64 | +``` |
| 65 | + |
| 66 | +### 構造体更新記法 |
| 67 | + |
| 68 | +既存のインスタンスの値を元に、一部だけ変更した新しいインスタンスを作成する場合、`..` 構文を使用できます。 |
| 69 | + |
| 70 | +```rust |
| 71 | +// user1のデータを元に、emailだけ変更したuser2を作成 |
| 72 | +let user2 = User { |
| 73 | + email: String::from( "[email protected]"), |
| 74 | + ..user1 // 残りのフィールドはuser1と同じ値が入る |
| 75 | +}; |
| 76 | +``` |
| 77 | + |
| 78 | +## タプル構造体とユニット様構造体 |
| 79 | + |
| 80 | +名前付きフィールドを持たない構造体も定義できます。 |
| 81 | + |
| 82 | +### タプル構造体 (Tuple Structs) |
| 83 | + |
| 84 | +フィールドに名前がなく、型だけが並んでいる構造体です。「型」として区別したい場合に便利です。例えば、同じ `(i32, i32, i32)` でも、「色」と「座標」は計算上混ぜるべきではありません。 |
| 85 | + |
| 86 | +```rust:tuple_structs.rs |
| 87 | +struct Color(i32, i32, i32); |
| 88 | +struct Point(i32, i32, i32); |
| 89 | + |
| 90 | +fn main() { |
| 91 | + let black = Color(0, 0, 0); |
| 92 | + let origin = Point(0, 0, 0); |
| 93 | + |
| 94 | + // black と origin は構造が同じでも、型としては別物なので |
| 95 | + // 関数に渡す際などにコンパイラが区別してくれます。 |
| 96 | + |
| 97 | + // 中身へのアクセスはタプル同様にインデックスを使用 |
| 98 | + println!("Origin X: {}", origin.0); |
| 99 | +} |
| 100 | +``` |
| 101 | + |
| 102 | +```rust-exec:tuple_structs.rs |
| 103 | +Origin X: 0 |
| 104 | +``` |
| 105 | + |
| 106 | +### ユニット様構造体 (Unit-like Structs) |
| 107 | + |
| 108 | +フィールドを全く持たない構造体です。`struct AlwaysEqual;` のように定義します。これらは、データを持たずに振る舞い(トレイト)だけを実装したい場合に役立ちますが、詳細は後の章で扱います。 |
| 109 | + |
| 110 | +## 所有権と構造体 |
| 111 | + |
| 112 | +構造体における所有権の扱いは非常に重要です。 |
| 113 | + |
| 114 | +1. **所有するデータ**: `String`や`Vec`のような所有権を持つ型をフィールドにすると、その構造体のインスタンスがデータの所有者になります。構造体がドロップされると、フィールドのデータもドロップされます。 |
| 115 | +2. **参照**: 構造体に「参照(\&strなど)」を持たせることも可能ですが、これには**ライフタイム(第12章)の指定が必要になります。そのため、現段階では構造体には(参照ではなく)`String`などの所有権のある型**を使うことを推奨します。 |
| 116 | + |
| 117 | +<!-- end list --> |
| 118 | + |
| 119 | +```rust |
| 120 | +// 推奨(現段階) |
| 121 | +struct User { |
| 122 | + username: String, // Userがデータを所有する |
| 123 | +} |
| 124 | + |
| 125 | +// 非推奨(ライフタイムの知識が必要になるためコンパイルエラーになる) |
| 126 | +// struct User { |
| 127 | +// username: &str, |
| 128 | +// } |
| 129 | +``` |
| 130 | + |
| 131 | +## impl ブロック:メソッドと関連関数 |
| 132 | + |
| 133 | +ここが、他言語のクラスにおける「メソッド定義」に相当する部分です。データ(`struct`)とは別に、`impl`(implementation)ブロックを使って振る舞いを定義します。 |
| 134 | + |
| 135 | +### メソッドの定義 |
| 136 | + |
| 137 | +メソッドは、最初の引数が必ず `self` (自分自身のインスタンス)になる関数です。 |
| 138 | + |
| 139 | + * `&self`: データを読み取るだけ(借用・不変)。最も一般的。 |
| 140 | + * `&mut self`: データを書き換える(借用・可変)。 |
| 141 | + * `self`: 所有権を奪う(ムーブ)。メソッド呼び出し後に元の変数は使えなくなる。変換処理などで使う。 |
| 142 | + |
| 143 | +<!-- end list --> |
| 144 | + |
| 145 | +```rust:rectangle.rs |
| 146 | +#[derive(Debug)] // print!で{:?}を使ってデバッグ表示するために必要 |
| 147 | +struct Rectangle { |
| 148 | + width: u32, |
| 149 | + height: u32, |
| 150 | +} |
| 151 | + |
| 152 | +// Rectangle型に関連する関数やメソッドを定義 |
| 153 | +impl Rectangle { |
| 154 | + // メソッド:面積を計算する(読み取りのみなので &self) |
| 155 | + fn area(&self) -> u32 { |
| 156 | + self.width * self.height |
| 157 | + } |
| 158 | + |
| 159 | + // メソッド:他の四角形を含めるか判定する |
| 160 | + fn can_hold(&self, other: &Rectangle) -> bool { |
| 161 | + self.width > other.width && self.height > other.height |
| 162 | + } |
| 163 | +} |
| 164 | + |
| 165 | +fn main() { |
| 166 | + let rect1 = Rectangle { width: 30, height: 50 }; |
| 167 | + let rect2 = Rectangle { width: 10, height: 40 }; |
| 168 | + |
| 169 | + // メソッド呼び出し(自動参照外し機能により、rect1.area() と書ける) |
| 170 | + println!("The area of the rectangle is {} pixels.", rect1.area()); |
| 171 | + |
| 172 | + if rect1.can_hold(&rect2) { |
| 173 | + println!("rect1 can hold rect2"); |
| 174 | + } |
| 175 | +} |
| 176 | +``` |
| 177 | + |
| 178 | +```rust-exec:rectangle.rs |
| 179 | +The area of the rectangle is 1500 pixels. |
| 180 | +rect1 can hold rect2 |
| 181 | +``` |
| 182 | + |
| 183 | +### 関連関数 (Associated Functions) |
| 184 | + |
| 185 | +`impl`ブロックの中で、第1引数に `self` を取らない関数も定義できます。これらはインスタンスではなく、型そのものに関連付けられた関数です。 |
| 186 | +他言語での「静的メソッド(Static Method)」に相当します。 |
| 187 | + |
| 188 | +最も一般的な用途は、コンストラクタのような役割を果たす初期化関数の作成です。Rustには `new` というキーワードはありませんが、慣習として `new` という名前の関連関数をよく作ります。 |
| 189 | + |
| 190 | +```rust:associated_fn.rs |
| 191 | +#[derive(Debug)] |
| 192 | +struct Circle { |
| 193 | + radius: f64, |
| 194 | +} |
| 195 | + |
| 196 | +impl Circle { |
| 197 | + // 関連関数(selfがない) |
| 198 | + // コンストラクタのように振る舞う |
| 199 | + fn new(radius: f64) -> Circle { |
| 200 | + Circle { radius } |
| 201 | + } |
| 202 | + |
| 203 | + // メソッド(selfがある) |
| 204 | + fn area(&self) -> f64 { |
| 205 | + 3.14159 * self.radius * self.radius |
| 206 | + } |
| 207 | +} |
| 208 | + |
| 209 | +fn main() { |
| 210 | + // 関連関数の呼び出しは :: を使う |
| 211 | + let c = Circle::new(2.0); |
| 212 | + |
| 213 | + println!("Circle radius: {}, area: {}", c.radius, c.area()); |
| 214 | +} |
| 215 | +``` |
| 216 | + |
| 217 | +```rust-exec:associated_fn.rs |
| 218 | +Circle radius: 2, area: 12.56636 |
| 219 | +``` |
| 220 | + |
| 221 | +## この章のまとめ |
| 222 | + |
| 223 | + * **構造体 (`struct`)** は関連するデータをまとめるカスタム型です。 |
| 224 | + * **タプル構造体** は名前付きフィールドを持たない構造体で、特定の型を区別するのに便利です。 |
| 225 | + * **メソッド** は `impl` ブロック内に定義し、第1引数に `self` を取ります。 |
| 226 | + * **関連関数** は `self` を取らず、`型名::関数名` で呼び出します(コンストラクタ `new` など)。 |
| 227 | + * Rustには継承がないため、データ構造とメソッドの組み合わせのみでオブジェクト指向的な設計を行います。 |
| 228 | + |
| 229 | +次章では、Rustの強力な機能の一つである「列挙型(Enum)」と、フロー制御の要である「パターンマッチ」について学びます。 |
| 230 | + |
| 231 | +### 練習問題1: RPGのキャラクター |
| 232 | + |
| 233 | +以下の要件を満たす `Character` 構造体と `impl` ブロックを作成してください。 |
| 234 | + |
| 235 | +1. フィールド: |
| 236 | + * `name`: キャラクター名 (`String`) |
| 237 | + * `hp`: 現在のヒットポイント (`i32`) |
| 238 | + * `attack_power`: 攻撃力 (`i32`) |
| 239 | +2. 関連関数 `new`: |
| 240 | + * 名前を受け取り、hpを100、attack\_powerを10で初期化したインスタンスを返す。 |
| 241 | +3. メソッド `take_damage`: |
| 242 | + * ダメージ量 (`i32`) を受け取り、`hp` から引く。ただし、`hp` は0未満にならないようにする(0で止める)。 |
| 243 | + * このメソッドは `hp` を変更するため、`&mut self` が必要です。 |
| 244 | + |
| 245 | +```rust:practice6_1.rs |
| 246 | +// ここにCharacter構造体とimplブロックを実装してください |
| 247 | + |
| 248 | + |
| 249 | +fn main() { |
| 250 | + let mut hero = Character::new(String::from("Hero")); |
| 251 | + println!("{} has {} HP.", hero.name, hero.hp); |
| 252 | + |
| 253 | + hero.take_damage(30); |
| 254 | + println!("After taking damage, {} has {} HP.", hero.name, hero.hp); |
| 255 | + |
| 256 | + hero.take_damage(80); |
| 257 | + println!("After taking more damage, {} has {} HP.", hero.name, hero.hp); |
| 258 | +} |
| 259 | +``` |
| 260 | +```rust-exec:practice6_1.rs |
| 261 | +Hero has 100 HP. |
| 262 | +After taking damage, Hero has 70 HP. |
| 263 | +After taking more damage, Hero has 0 HP. |
| 264 | +``` |
| 265 | + |
| 266 | +### 練習問題2: 座標計算 |
| 267 | + |
| 268 | +2次元座標を表すタプル構造体 `Point(f64, f64)` を作成し、以下を実装してください。 |
| 269 | + |
| 270 | +1. 関連関数 `origin`: 原点 `(0.0, 0.0)` を持つ `Point` を返す。 |
| 271 | +2. メソッド `distance_to`: 別の `Point` への参照を受け取り、2点間の距離を計算して返す `f64`。 |
| 272 | + * ヒント: 距離の公式は sqrt((x₂ - x₁)² + (y₂ - y₁)²) です。平方根は `f64`型の値に対して `.sqrt()` メソッドで計算できます。 |
| 273 | + |
| 274 | +```rust:practice6_2.rs |
| 275 | +// ここにPointタプル構造体とimplブロックを実装してください |
| 276 | + |
| 277 | + |
| 278 | +fn main() { |
| 279 | + let p1 = Point::origin(); |
| 280 | + let p2 = Point(3.0, 4.0); |
| 281 | + |
| 282 | + let distance = p1.distance_to(&p2); |
| 283 | + println!("Distance from origin to p2 is {}", distance); |
| 284 | +} |
| 285 | +``` |
| 286 | +```rust-exec:practice6_2.rs |
| 287 | +Distance from origin to p2 is 5.0 |
| 288 | +``` |
0 commit comments