Swift: Swiftクラスと構造体:値型と参照型の選択
クラ��と構造体はSwiftでデータモデルを構築するための2つの中核型です。多くの類似点を共有しますが、メモリ管理と値の受け渡し方法が根本的に異なります。このレッスンでは、それぞれの選択戦略を明確にします。
1. 学習目標
- クラスと構造体を定義する基本構文
- 値型(構造体)と参照型(クラス)の核心的な違い
mutatingキーワードの目的- クラスと構造体の初期化ルール
- クラスと構造体の使い分け
2. iOS開発者の実話
(1) 課題:ある場所でユーザーアカウントを変更すると別の場所にも影響する
Aliceはユーザーアカウントのモデルにクラスを選択しました:
SWIFT
class UserAccount {
var name: String
var balance: Double
init(name: String, balance: Double) {
self.name = name
self.balance = balance
}
}
let account1 = UserAccount(name: "Alice", balance: 1000)
let account2 = account1 // References the same instance
account2.name = "Bob"
print(account1.name) // "Bob" — account1 was also changed!
Aliceは新しいユーザーを作成したかっただけなのに、元のデータを誤って変更してしまいました。参照型の副作用がコードベース全体に気付かれずに伝播しました。
(2) 解決策:値型としての構造体
SWIFT
struct UserAccount {
var name: String
var balance: Double
}
var account1 = UserAccount(name: "Alice", balance: 1000)
var account2 = account1 // Copies an independent set of data
account2.name = "Bob"
print(account1.name) // "Alice" — account1 is unaffected
構造体の代入は値のコピーを行います — 各インスタンスは独立しています。
(3) 利点:予測可能なデータ動作
| 観点 | class(参照型) | struct(値型) |
|---|---|---|
| 代入の動作 | 同じインスタンスを共有 | 独立したコピー |
| 誤変更のリスク | 高い(エイリアシング) | なし |
| メモリ割り当て | ヒープ割り当て | スタック割り当て(より効率的) |
| スレッド安全性 | 手動同期が必要 | 自然に分離 |
| 適している場面 | 共有可変状態 | データモデル、値セマンティクス |
3. クラスと構造体の定義
(1) 基本構文の比較
graph TB
subgraph class
A["class Person {"]
B[" var name: String"]
C[" var age: Int"]
D[" func greet() { }"]
E["}"]
end
subgraph struct
F["struct Person {"]
G[" var name: String"]
H[" var age: Int"]
I[" func greet() { }"]
J["}"]
end
| 特徴 | class | struct | 備考 |
|---|---|---|---|
| プロパティ | 可 | 可 | ストアドプロパティとコンピューテッドプロパティ |
| メソッド | 可 | 可 | インスタンスメソッドと型メソッド |
| イニシャライザ | 可 | 可 | classはメンバーワイズinitを自動生成しない |
| 継承 | 可 | 不可 | structは継承不可 |
| デイニシャライザ | 可 | 不可 | structにdeinitはない |
| 参照カウント | 可 | 不可 | structはARC不要 |
| 型キャスト | 可 | 不可 | as?/as! はclassのみ |
▶ サンプル: クラスと構造体の定義
SWIFT
// ============================================
// Class and struct definition comparison
// ============================================
// Class definition — must write init manually
class PersonClass {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
func introduce() {
print("Hi, I'm \(name), \(age) years old")
}
}
// Struct definition — automatically gets a memberwise initializer
struct PersonStruct {
var name: String
var age: Int
func introduce() {
print("Hi, I'm \(name), \(age) years old")
}
}
let p1 = PersonClass(name: "Alice", age: 28)
p1.introduce()
let p2 = PersonStruct(name: "Bob", age: 22) // Auto-generated init
p2.introduce()
出力:
TEXT 📖 参照専用Hi, I'm Alice, 28 years old Hi, I'm Bob, 22 years old
4. 値型 vs 参照型
(1) 核心的な違い
graph LR
subgraph "値型 (struct)"
A["var a = Point(x: 1, y: 2)"]
B["var b = a"]
C["b.x = 5"]
D["a.x → 1 (独立)"]
A --> B --> C --> D
end
subgraph "参照型 (class)"
E["var a = Point(x: 1, y: 2)"]
F["var b = a"]
G["b.x = 5"]
H["a.x → 5 (共有)"]
E --> F --> G --> H
end
| 観点 | 値型 (struct) | 参照型 (class) |
|---|---|---|
| 代入のセマンティクス | 内容をコピー | 参照をコピー |
| 等価性チェック | == が値を比較 |
=== が同一性を比較 |
| 格納場所 | スタック(高速) | ヒープ(ARC管理) |
| let定数 | プロパティが不変 | 参照は不変だがプロパティは変更可能 |
| 関数パラメータ | デフォルトでコピー | デフォルトで参照を共有 |
▶ サンプル: 代入動作の比較
SWIFT
// ============================================
// Value type vs reference type assignment behavior
// ============================================
// Struct — value type
struct Position {
var x: Int
var y: Int
}
var posA = Position(x: 10, y: 20)
var posB = posA
posB.x = 99
print("posA: (\(posA.x), \(posA.y))") // Unchanged
print("posB: (\(posB.x), \(posB.y))") // Only affects posB
// Class — reference type
class Location {
var x: Int
var y: Int
init(x: Int, y: Int) { self.x = x; self.y = y }
}
var locA = Location(x: 10, y: 20)
var locB = locA
locB.x = 99
print("locA: (\(locA.x), \(locA.y))") // Also changed!
print("locB: (\(locB.x), \(locB.y))")
出力:
TEXT 📖 参照専用posA: (10, 20) posB: (99, 20) locA: (99, 20) locB: (99, 20)
(2) 同一性演算子 ===
参照型は === を使って2つの変数が同じインスタンスを指しているかをチェックできます:
SWIFT
let loc1 = Location(x: 5, y: 10)
let loc2 = loc1
let loc3 = Location(x: 5, y: 10) // Different instance
print(loc1 === loc2) // true — same instance
print(loc1 === loc3) // false — different instances
ヒント:
===は同一性演算子で、==(等価性演算子)とは異なります。値型は==のみ使用可能(Equatableを実装する必要あり)で、===は使用できません。
5. mutatingメソッドと選択戦略
(1) mutating キーワード
構造体のインスタンスメソッドはデフォルトではプロパティを変更できません。変更するには mutating をマークする必要があります:
| メソッドタイプ | class | struct(非mutating) | struct(mutating) |
|---|---|---|---|
| プロパテ��変更 | 許可 | コンパイルエラー | 許可 |
| self変更 | 許可 | 不可 | 許可 |
| 呼び出し要件 | 制限なし | let定数で呼び出し可能 | varで呼び出す必要あり |
▶ サンプル: mutatingメソッド
SWIFT
// ============================================
// Struct mutating methods
// ============================================
struct Counter {
var count = 0
// Non-mutating — read-only method
func display() {
print("Count: \(count)")
}
// mutating — modifies property
mutating func increment() {
count += 1
}
// mutating — replaces the entire instance
mutating func reset() {
self = Counter() // Replace self
}
}
var counter = Counter() // Must be var to call mutating methods
counter.increment()
counter.increment()
counter.display() // Count: 2
counter.reset()
counter.display() // Count: 0
出力:
TEXT 📖 参照専用Count: 2 Count: 0
(2) 選択戦略
| シナリオ | 推奨 | 理由 |
|---|---|---|
| データモデル(Point, Size, User) | struct | 値セマンティクス、安全で予測可能 |
| 継承が必要(UIViewサブクラス, BaseViewController) | class | 継承はクラスのみ |
| 共有可変状態が必要 | class | 参照型は自動的に共有 |
| ObjC相互運用が必要 | class | @objc はクラスが必要 |
| 軽量ラッパー(少数の値をラップ) | struct | スタック割り当て、オーバーヘッドゼロ |
| deinitクリーンアップが必要 | class | structにdeinitはない |
▶ サンプル: モデルに構造体をデフォルトで使う
SWIFT
// ============================================
// Why modern Swift prefers struct for models
// ============================================
struct User {
let id: Int
var name: String
var email: String
var score: Int
mutating func addScore(_ points: Int) {
score += points
}
}
var user = User(id: 1, name: "Alice", email: "alice@example.com", score: 100)
user.addScore(50)
// Copy-and-mutate pattern — safely create changed versions
let admin = User(id: 2, name: "Admin", email: "admin@example.com", score: 0)
// admin.addScore(10) // Compile error! let constant cannot call mutating methods
var editor = admin
editor.name = "Editor" // Independent modification, doesn't affect admin
出力:
TEXT 📖 参照専用(No console output — demonstrates value semantics: editor.name = "Editor" does not affect admin.name)
6. 完全な例:銀行口座システム
SWIFT
// ============================================
// Complete example: Bank account system
// Features: class for account management, struct for transaction records
// ============================================
import Foundation
// 1. Transaction record — value type struct
struct Transaction {
let id: Int
let amount: Double
let type: String // "deposit" or "withdraw"
let date: Date
func description() -> String {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
return "[\(formatter.string(from: date))] \(type): $\(amount)"
}
}
// 2. Account — reference type class (shared state)
class BankAccount {
let accountNumber: String
var balance: Double
private var transactions: [Transaction] = []
private var nextId = 1
init(accountNumber: String, initialBalance: Double = 0) {
self.accountNumber = accountNumber
self.balance = initialBalance
}
func deposit(amount: Double) {
guard amount > 0 else { return }
balance += amount
let tx = Transaction(id: nextId, amount: amount, type: "deposit", date: Date())
transactions.append(tx)
nextId += 1
}
func withdraw(amount: Double) -> Bool {
guard amount > 0 && amount <= balance else { return false }
balance -= amount
let tx = Transaction(id: nextId, amount: amount, type: "withdraw", date: Date())
transactions.append(tx)
nextId += 1
return true
}
func printStatement() {
print("=== Account \(accountNumber) ===")
print("Balance: $\(balance)")
print("Transactions:")
for tx in transactions {
print(" \(tx.description())")
}
}
}
// 3. Usage
let account = BankAccount(accountNumber: "ACC-001", initialBalance: 1000)
account.deposit(amount: 500)
account.withdraw(amount: 200)
account.deposit(amount: 100)
account.printStatement()
出力:
TEXT 📖 参照専用=== Account ACC-001 === Balance: $1400.0 Transactions: [2026-07-30] deposit: $500.0 [2026-07-30] withdraw: $200.0 [2026-07-30] deposit: $100.0
❓ よくある質問
Q Appleが構造体を優先するよう推奨する理由は?
A 値型はより安全で予測可能です。共有参照による副作用がなく、循環参照もなく、参照カウントのオーバーヘッドもありません。String、Array、DictionaryなどのSwift標準ライブラリの型はすべて構造体です。
Q 構造体にはパフォーマンス上の利点がありますか?
A はい。構造体はスタックに割り当てられ、参照カウントのオーバーヘッドがなく、ヒープ割り当てのコストもあり��せん。ただし、大きな構造体を関数間で受け渡すと、参照を渡す場合よりもコピーコストが高くなる可能性があります。Swiftのコピーオンライト機構がこれを最適化します。
Q クラスのinitは常に手動で書く必要がありますか?
A クラスの場合、すべてのプロパティにデフォルト値があり、イニシャライザが定義されていない場合、Swiftはデフォルトの引数なしinitを生成します。しかし、構造体のようにメンバーワイズinitが自動生成されることは決してありません。
Q 構造体にlazyプロパティは持てますか?
A はい。
lazy var は構造体でも動作しますが、lazyプロパティは不変の構造体でアクセスできないため、実用的な使用は制限されます。lazyはクラスでより自然です。Q mutatingはクラスで使えますか?
A いいえ。
mutating は値型(構造体と列挙型)にのみ適用されます。クラスのメソッドはデフォルトでプロパティを変更できます — mutating キーワードは不要です。📖 まとめ
- クラスと構造体はどちらもプロパティ、メソッド、サブスクリプト、エクステンションを定義できる
- 構造体は値型(代入時にコピー)、クラスは参照型(参照をコピー)
- 構造体はメンバーワイズイニシャライザが自動生成されるが、クラスは手動でinitを書く必要がある
mutatingキーワードにより構造体メソッドが自身のプロパティを変更できる===を使って2つの参照型変数が同じインスタンスを指しているかをチェックする- 構造体を優先し、継承や共有可変状態が必要な場合にクラスを使う
📝 練習問題
- 基本:
title、author、pagesプロパティとsummary()メソッドを持つBook構造体を定義してください。2つのインスタンスを作成し、値型のコピー動作をテストしてください。 - 中級:
currentTempプロパティとreadings: [Double]履歴配列を持つTemperatureSensorクラスを定義して��ださい。recordTemp(_:)とaverage()メソッドを実装してください。インスタンスを作成し、5つの温度を記録し、平均を出力してください。 - 発展: 「チームコラボレーションシステム」を設計してください:共有メンバーリストを管理する
Teamクラスと、個別プロファイル用のMember構造体を使用します。TeamはaddMember(_:)とlistMembers()メソッドを提供します。Memberはロールを更新するmutatingメソッドを持ちます。クラスの共��参照動作を検証してください。