Swift: Swiftジェネリックチュートリアル:ゼロから学ぶジェネリック関数と型制約
ジェネリックを使うと、任意の型で動作する柔軟なコードを書けます。型ごとに同じロジックを重複して書く必要がなくなります — ��る種の万能金型のようなもので、どんな材料を流し込んでも適切な形を作り出せます。
1. 学習目標
- ジェネリック関数とジェネリック型の定義方法
- 型制約がジェネリックパラメータの範囲を制限する方法
- プロトコルにおける関連型
- where句の高度な使用法
- 標準ライブラリにおけるジェネリックの実践的な応用
2. バックエンドエンジニアの実話
(1) 課題:すべての型に同じロジックを書く
Charlieはキャッシュサービスを構築しており、後入れ先出しのスタックデータ構造が必要でした。彼はまずInt版を書きました:
struct IntStack {
private var items: [Int] = []
mutating func push(_ item: Int) { items.append(item) }
mutating func pop() -> Int? { items.popLast() }
}
しかしすぐにString、Double、さらにはカスタムのUser型もサポートする必要が出てきました。Charlieはコピーアンドペーストを繰り返すことになりました — 型名だけが変わり、ロジックは同一でした。
(2) ジェネリックによる解決策
ジェネリックは型をパラメータに変えます — 1つの実装ですべてに対応します:
struct Stack<Element> {
private var items: [Element] = []
mutating func push(_ item: Element) { items.append(item) }
mutating func pop() -> Element? { items.popLast() }
}
これで Stack<Int>、Stack<String>、Stack<User> がすべて同じ実装を使い、コンパイラが型安全性を保証します。
(3) 利点:コード量60%削減
| 観点 | 非ジェネリックアプローチ | ジェネリックアプローチ |
|---|---|---|
| コード行数 | 120行(3つの型) | 20行(1つのジェネリック) |
| 新しい型の追加コスト | 40行のコピーペースト | 1行:Stack<NewType> |
| 型安全性 | 各型独立 | コンパイル時チェック |
| 保守コスト | 3箇所でロジック変更 | 1箇所のみ変更 |
3. ジェネリック関数
ジェネリック関数を使うと、関数定義でプレースホルダ型を使用でき、具体的な型は呼び出し箇所で決定されます。
graph LR
A["func swap<T>(a: inout T, b: inout T)"] --> B["Intで呼び出し"]
A --> C["Stringで呼び出し"]
A --> D["Doubleで呼び出し"]
B --> E["T = Int: 安全なスワップ"]
C --> F["T = String: 安全なスワップ"]
D --> G["T = Double: 安全なスワップ"]
(1) ジェネリックパラメータの構文
ジェネリックパラメータは関数名の後に山括弧 <> で記述し、慣例として大文字の T、U、V をプレースホルダとして使用します。
| 構文 | 意味 | 例 |
|---|---|---|
<T> |
単一のジェネリックパラメータ | func identity<T>(_ value: T) -> T |
<T, U> |
2つのジェネリックパラメータ | func pair<T, U>(_ a: T, _ b: U) -> (T, U) |
<T: Equatable> |
制約付きジェネリック | func isEqual<T: Equatable>(_ a: T, _ b: T) -> Bool |
▶ サンプル: 2つの変数値の交換
// ============================================
// Generic function: swap two values of any type
// ============================================
func swapValues<T>(_ a: inout T, _ b: inout T) {
let temp = a
a = b
b = temp
}
var x = 10
var y = 20
swapValues(&x, &y)
print("x = \(x), y = \(y)")
var firstName = "Alice"
var lastName = "Bob"
swapValues(&firstName, &lastName)
print("firstName = \(firstName), lastName = \(lastName)")
出力:
TEXT 📖 参照専用x = 20, y = 10 firstName = Bob, lastName = Alice
(2) 複数のジェネリックパラメータ
関数は複数のジェネリックパラメータを持つことができ、それぞれが異なる位置で異なる型を表します。
▶ サンプル: キーバリューペアの構築
// ============================================
// Multiple generic parameters to build a key-value pair
// ============================================
func makePair<K, V>(_ key: K, _ value: V) -> (K, V) {
return (key, value)
}
let pair1 = makePair("id", 1001)
let pair2 = makePair(3.14, "Pi")
print(pair1)
print(pair2)
出力:
TEXT 📖 参照専用(id, 1001) (3.14, Pi)
4. ジェネリック型
ジェネリック型を使うと、プレースホルダを持つ構造体、クラス、列挙型を定義でき、任意の型のデータを処理できます。
graph TB
A["Stack<Element>"] --> B["push(Element)"]
A --> C["pop() -> Element?"]
A --> D["peek() -> Element?"]
A --> E["count: Int"]
B --> F["itemsに追加"]
C --> G["itemsから最後を削除"]
D --> H["items.lastを返す"]
(1) ジェネリック構造体
struct Stack<Element> { ... }
| ユースケース | 構文 | 説明 |
|---|---|---|
| Intスタック | Stack<Int> |
Int値のみプッシュ/ポップ可能 |
| Stringスタック | Stack<String> |
String値のみプッシュ/ポップ可能 |
| カスタム型スタック | Stack<User> |
User値のみプッシュ/ポップ可能 |
▶ サンプル: ジェネリックスタックデータ構造
// ============================================
// Generic stack: supports last-in-first-out for any type
// ============================================
struct Stack<Element> {
private var items: [Element] = []
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element? {
items.popLast()
}
func peek() -> Element? {
items.last
}
var count: Int { items.count }
}
var intStack = Stack<Int>()
intStack.push(10)
intStack.push(20)
intStack.push(30)
print("Pop: \(intStack.pop() ?? 0)")
var stringStack = Stack<String>()
stringStack.push("Alice")
stringStack.push("Bob")
print("Peek: \(stringStack.peek() ?? "")")
出力:
TEXT 📖 参照専用Pop: 30 Peek: Bob
(2) ジェネリックExtension
ジェネリック型をextensionする場合、型パラメータを再宣言する必要はありません — プレースホルダ名をそのまま使用します。
▶ サンプル: ジェネリックスタックにmapメソッドを追加
// ============================================
// Adding map functionality to Stack via extension
// ============================================
extension Stack {
func map<T>(_ transform: (Element) -> T) -> Stack<T> {
var result = Stack<T>()
for item in items {
result.push(transform(item))
}
return result
}
}
var s = Stack<Int>()
s.push(1); s.push(2); s.push(3)
let doubled = s.map { $0 * 2 }
print("Count: \(doubled.count)")
出力:
TEXT 📖 参照専用Count: 3
5. 型制約と関連型
型制約はジェネリックパラメータが特定の条件を満たすように制限します。関連型はプロトコル内の「ジェネリックプレースホルダ」で、実装者が指定します。
graph TB
A["<T: Equatable>"] --> B["== が使える"]
A --> C["func findIndex<T: Equatable>(of: T, in: [T]) -> Int?"]
B --> D["Int =="]
B --> E["String =="]
B --> F["Equatable準拠のカスタム型"]
(1) 型制約
| 制約構文 | 意味 | ユースケース |
|---|---|---|
<T: Equatable> |
Tは等価比較可能でなければならない | 要素検索、重複除去 |
<T: Hashable> |
Tはハッシュ可能でなければならない | Dictionaryキー |
<T: Comparable> |
Tは比較可能でなければならない | ソート、最小/最大検索 |
<T: Codable> |
Tはエンコード/デコード可能でなければならない | JSONシリアライゼーション |
▶ サンプル: Equatable制約を使った要素検索
// ============================================
// Constraining generic parameter to be equatable
// ============================================
func findIndex<T: Equatable>(of value: T, in array: [T]) -> Int? {
for (index, item) in array.enumerated() {
if item == value {
return index
}
}
return nil
}
let numbers = [10, 20, 30, 40]
if let index = findIndex(of: 30, in: numbers) {
print("Found at index \(index)")
}
let names = ["Alice", "Bob", "Charlie"]
if let index = findIndex(of: "Bob", in: names) {
print("Found at index \(index)")
}
出力:
TEXT 📖 参照専用Found at index 2 Found at index 1
(2) 関連型とwhere句
associatedtype キーワードにより、プロトコルがジェネリックをサポートできるようになります — プロトコル自体は具体的な型を指定せず、実装者が決定します。
▶ サンプル: 関連型プロトコルとwhere句
// ============================================
// Associated type protocol + where clause
// ============================================
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
struct Box<T>: Container {
typealias Item = T
private var items: [T] = []
mutating func append(_ item: T) { items.append(item) }
var count: Int { items.count }
subscript(i: Int) -> T { items[i] }
}
func allItemsMatch<C1: Container, C2: Container>(
_ c1: C1, _ c2: C2
) -> Bool where C1.Item == C2.Item, C1.Item: Equatable {
guard c1.count == c2.count else { return false }
for i in 0..<c1.count {
if c1[i] != c2[i] { return false }
}
return true
}
var box1 = Box<Int>()
box1.append(1); box1.append(2); box1.append(3)
var box2 = Box<Int>()
box2.append(1); box2.append(2); box2.append(3)
print("All match: \(allItemsMatch(box1, box2))")
出力:
TEXT 📖 参照専用All match: true
6. 完全な例:ジェネリックキューとキャッシュシステム
// ============================================
// Full example: Generic queue + bounded cache
// Features: general-purpose queue, cache wrapper, statistics
// ============================================
import Foundation
// 1. Generic queue
struct Queue<Element> {
private var items: [Element] = []
mutating func enqueue(_ item: Element) { items.append(item) }
mutating func dequeue() -> Element? { items.isEmpty ? nil : items.removeFirst() }
var count: Int { items.count }
}
// 2. Generic cache (with capacity limit)
class Cache<Key: Hashable, Value> {
private var storage: [Key: Value] = [:]
private let capacity: Int
init(capacity: Int = 100) {
self.capacity = capacity
}
func set(_ value: Value, for key: Key) {
if storage.count >= capacity {
storage.removeFirst()
}
storage[key] = value
}
func get(for key: Key) -> Value? { storage[key] }
var count: Int { storage.count }
}
// 3. Usage example
var queue = Queue<String>()
queue.enqueue("Task 1")
queue.enqueue("Task 2")
queue.enqueue("Task 3")
print("Queue count: \(queue.count)")
print("Dequeue: \(queue.dequeue() ?? "")")
let cache = Cache<String, Int>(capacity: 3)
cache.set(42, for: "answer")
cache.set(100, for: "score")
print("Cached answer: \(cache.get(for: "answer") ?? 0)")
print("Cache count: \(cache.count)")
出力:
TEXT 📖 参照専用Queue count: 3 Dequeue: Task 1 Cached answer: 42 Cache count: 2
❓ よくある質問
Array<Element> で、Elementがジェネリックパラメータです。すべての配列操作(append、sort、map)はジェネリックメソッドです。📖 まとめ
- ジェネリック関数は
<T>を使って型パラメータを宣言し、具体的な型は呼び出し箇所で推論される - ジェネリック型(構造体/クラス/列挙型)は1つの実装で任意の型のデータを処理できる
- 型制約
<T: Protocol>はジェネリックパラメータの範囲を制限し、型安全性を確保する - 関連型
associatedtypeによりプロトコルがジェネリックをサポートし、実装者が具体的な型を決定する - where句は複雑な複数条件の型関係を表現する
- 標準ライブラリはジェネリックを多用:Array、Dictionary、Optionalはすべてジェネリック型
📝 練習問題
- 基本: 2つのパラメータを受け取り、大きい方を返すジェネリック関数
findMax<T: Comparable>を書いてください。Int、Double、String型でテストしてください。 - 中級: 第4節の
Stack型を拡張し、(Element) -> Boolクロージャを受け取り、フィルタされた新しいスタックを返すfilterメソッドを追加してください。 - 発展: ジェネリックを使って
write(_:)とread() -> Element?メソッド、固定容量、満杯時に最も古いデータを自動上書きするRingBuffer<Element>循環バッファを実装してください。