Swift: Swift Generics Tutorial

Generics let you write flexible code that works with any type, avoiding the need to duplicate the same logic for each type -- like a universal mold that produces the right shape no matter what material you pour in.

1. What You'll Learn


2. A Backend Engineer's Real Story

(1) Pain Point: Writing the Same Logic for Every Type

Charlie was building a caching service and needed a last-in-first-out stack data structure. He first wrote a version for Int:

SWIFT
struct IntStack {
    private var items: [Int] = []
    mutating func push(_ item: Int) { items.append(item) }
    mutating func pop() -> Int? { items.popLast() }
}

But soon he needed to support String, Double, and even a custom User type. Charlie found himself copying and pasting -- only the type name changed; the logic was identical.

(2) The Generic Solution

Generics turn the type into a parameter -- one implementation fits all:

SWIFT
struct Stack<Element> {
    private var items: [Element] = []
    mutating func push(_ item: Element) { items.append(item) }
    mutating func pop() -> Element? { items.popLast() }
}

Now Stack<Int>, Stack<String>, and Stack<User> all use the same implementation, with the compiler ensuring type safety.

(3) Benefits: 60% Less Code

Dimension Non-Generic Approach Generic Approach
Lines of code 120 lines (3 types) 20 lines (1 generic)
Cost of adding a new type 40 lines of copy-paste 1 line: Stack<NewType>
Type safety Each type independent Compile-time checked
Maintenance cost Change logic in 3 places Change only in 1 place

3. Generic Functions

Generic functions let you use placeholder types in function definitions; the concrete type is determined at the call site.

100%
graph LR
    A["func swap<T>(a: inout T, b: inout T)"] --> B["Called with Int"]
    A --> C["Called with String"]
    A --> D["Called with Double"]
    B --> E["T = Int: safe swap"]
    C --> F["T = String: safe swap"]
    D --> G["T = Double: safe swap"]

(1) Generic Parameter Syntax

Generic parameters are written in angle brackets <> after the function name, conventionally using uppercase letters T, U, V as placeholders.

Syntax Meaning Example
<T> Single generic parameter func identity<T>(_ value: T) -> T
<T, U> Two generic parameters func pair<T, U>(_ a: T, _ b: U) -> (T, U)
<T: Equatable> Constrained generic func isEqual<T: Equatable>(_ a: T, _ b: T) -> Bool

▶ Example: Swapping Two Variable Values

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)")

Output:

TEXT 📖 Display only
x = 20, y = 10
firstName = Bob, lastName = Alice

(2) Multiple Generic Parameters

Functions can have multiple generic parameters, each representing a different type at a different position.

▶ Example: Building a Key-Value Pair

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)

Output:

TEXT 📖 Display only
(id, 1001)
(3.14, Pi)

4. Generic Types

Generic types let you define structs, classes, or enums with placeholders, enabling them to handle data of any type.

100%
graph TB
    A["Stack<Element>"] --> B["push(Element)"]
    A --> C["pop() -> Element?"]
    A --> D["peek() -> Element?"]
    A --> E["count: Int"]
    B --> F["append to items"]
    C --> G["remove last from items"]
    D --> H["return items.last"]

(1) Generic Structs

SWIFT
struct Stack<Element> { ... }
Use Case Syntax Description
Int stack Stack<Int> Can only push/pop Int values
String stack Stack<String> Can only push/pop String values
Custom type stack Stack<User> Can only push/pop User values

▶ Example: Generic Stack Data Structure

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() ?? "")")

Output:

TEXT 📖 Display only
Pop: 30
Peek: Bob

(2) Generic Extensions

When extending a generic type, you don't need to redeclare the type parameter -- just use the placeholder name directly.

▶ Example: Adding a map Method to the Generic Stack

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)")

Output:

TEXT 📖 Display only
Count: 3

5. Type Constraints and Associated Types

Type constraints restrict generic parameters to satisfy specific conditions. Associated types are "generic placeholders" in protocols, specified by the implementer.

100%
graph TB
    A["<T: Equatable>"] --> B["Can use =="]
    A --> C["func findIndex<T: Equatable>(of: T, in: [T]) -> Int?"]
    B --> D["Int =="]
    B --> E["String =="]
    B --> F["Custom with Equatable"]

(1) Type Constraints

Constraint Syntax Meaning Use Case
<T: Equatable> T must be equatable Finding elements, deduplication
<T: Hashable> T must be hashable Dictionary keys
<T: Comparable> T must be comparable Sorting, finding min/max
<T: Codable> T must be codable/decodable JSON serialization

▶ Example: Using Equatable Constraint to Find an Element

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)")
}

Output:

TEXT 📖 Display only
Found at index 2
Found at index 1

(2) Associated Types and where Clauses

The associatedtype keyword lets protocols support generics -- the protocol itself doesn't specify a concrete type; the implementer decides.

▶ Example: Protocol with Associated Type and where Clause

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))")

Output:

TEXT 📖 Display only
All match: true

6. Full Example: Generic Queue and Cache System

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)")

Output:

TEXT 📖 Display only
Queue count: 3
Dequeue: Task 1
Cached answer: 42
Cache count: 2

❓ FAQ

Q What's the difference between generics and Any?
A Generics preserve type information and are compile-time type safe. Any checks types at runtime. Generics mean "the compiler knows the concrete type"; Any means "the compiler gives up on type checking."
Q When should I use generic parameters vs. protocol constraints?
A Generic parameters work for "type can be arbitrary but operations are consistent" scenarios. Protocol constraints work for "type must satisfy a specific interface" scenarios. The two are often used together.
Q What's the difference between associated types and generic parameters?
A Associated types are defined inside a protocol and specified by the implementer. Generic parameters are defined on a function/type and specified by the caller. Associated types are essentially "the protocol's generic parameters."
Q How is the generic Array implemented?
A The standard library's Array is a generic struct Array<Element>, where Element is the generic parameter. All array operations (append, sort, map) are generic methods.
Q Do generics impact performance?
A No. Swift uses "generic specialization" -- the compiler generates specialized versions for each concrete type, so there is no runtime overhead from generics.

📖 Summary


📝 Exercises

  1. Basic: Write a generic function findMax<T: Comparable> that accepts two parameters and returns the larger one. Test with Int, Double, and String types.
  2. Intermediate: Extend the Stack type from Section 4 by adding a filter method that accepts a (Element) -> Bool closure and returns a filtered new stack.
  3. Challenge: Use generics to implement a RingBuffer<Element> circular buffer with write(_:) and read() -> Element? methods, a fixed capacity, and automatic overwriting of the oldest data when full.
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏