Swift: Swiftジェネリックチュートリアル:ゼロから学ぶジェネリック関数と型制約

ジェネリックを使うと、任意の型で動作する柔軟なコードを書けます。型ごとに同じロジックを重複して書く必要がなくなります — ��る種の万能金型のようなもので、どんな材料を流し込んでも適切な形を作り出せます。

1. 学習目標


2. バックエンドエンジニアの実話

(1) 課題:すべての型に同じロジックを書く

Charlieはキャッシュサービスを構築しており、後入れ先出しのスタックデータ構造が必要でした。彼はまずInt版を書きました:

SWIFT
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つの実装ですべてに対応します:

SWIFT
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. ジェネリック関数

ジェネリック関数を使うと、関数定義でプレースホルダ型を使用でき、具体的な型は呼び出し箇所で決定されます。

100%
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) ジェネリックパラメータの構文

ジェネリックパラメータは関数名の後に山括弧 <> で記述し、慣例として大文字の TUV をプレースホルダとして使用します。

構文 意味
<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つの変数値の交換

SWIFT
// ============================================
// 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) 複数のジェネリックパラメータ

関数は複数のジェネリックパラメータを持つことができ、それぞれが異なる位置で異なる型を表します。

▶ サンプル: キーバリューペアの構築

SWIFT
// ============================================
// 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. ジェネリック型

ジェネリック型を使うと、プレースホルダを持つ構造体、クラス、列挙型を定義でき、任意の型のデータを処理できます。

100%
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) ジェネリック構造体

SWIFT
struct Stack<Element> { ... }
ユースケース 構文 説明
Intスタック Stack<Int> Int値のみプッシュ/ポップ可能
Stringスタック Stack<String> String値のみプッシュ/ポップ可能
カスタム型スタック Stack<User> User値のみプッシュ/ポップ可能

▶ サンプル: ジェネリックスタックデータ構造

SWIFT
// ============================================
// 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メソッドを追加

SWIFT
// ============================================
// 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. 型制約と関連型

型制約はジェネリックパラメータが特定の条件を満たすように制限します。関連型はプロトコル内の「ジェネリックプレースホルダ」で、実装者が指定します。

100%
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制約を使った要素検索

SWIFT
// ============================================
// 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句

SWIFT
// ============================================
// 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. 完全な例:ジェネリックキューとキャッシュシステム

SWIFT
// ============================================
// 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

❓ よくある質問

Q ジェネリックとAnyの違いは何ですか?
A ジェネリックは型情報を保持し、コンパイル時に型安全です。Anyは実行時に型をチェックします。ジェネリックは「コンパイラが具体的な型を知っている」ことを意味し、Anyは「コンパイラが型チェックを諦める」ことを意味します。
Q ジェネリックパラメータとプロトコル制約はいつ使い分けるべきですか?
A ジェネリックパラメータは「型は任意だが操作は一貫している」シナリオに適します。プロ��コル制約は「型が特定のインターフェースを満たす必要がある」シナリオに適します。両者はよく併用されます。
Q 関連型とジェネリックパラメータの違いは何ですか?
A 関連型はプロトコル内部で定義され、実装者が指定します。ジェネリックパラメータは関数/型で定義され、呼び出し側が指定します。関連型は本質的に「プロトコルのジェネリックパラメータ」です。
Q ジェネリックArrayはどのように実装されていますか?
A 標準ライブラリのArrayはジェネリック構造体 Array<Element> で、Elementがジェネリックパラメータです。すべての配列操作(append、sort、map)はジェネリックメソッドです。
Q ジェネリックはパフォーマンスに影響しますか?
A いいえ。Swiftは「ジェネリック特殊化」を使用します — コンパイラが各具体的な型に対して特殊化されたバージョンを生成するため、ジェネリックによるランタイムオーバーヘッドはありません。

📖 まとめ


📝 練習問題

  1. 基本: 2つのパラメータを受け取り、大きい方を返すジェネリック関数 findMax<T: Comparable> を書いてください。Int、Double、String型でテストしてください。
  2. 中級: 第4節の Stack 型を拡張し、(Element) -> Bool クロージャを受け取り、フィルタされた新しいスタックを返す filter メソッドを追加してください。
  3. 発展: ジェネリックを使って write(_:)read() -> Element? メソッド、固定容量、満杯時に最も古いデータを自動上書きする RingBuffer<Element> 循環バッファを実装してください。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%