Swift Generics

Generics let you write flexible, reusable code that works with any type. Instead of writing the same function three times — once for Int, once for String, once for Double — you write it once using a placeholder type, and Swift fills in the real type when you call it. Think of a generic function like a universal power adapter: one design that fits any country's outlet.

The Problem Generics Solve

Without generics, you repeat yourself for every type:

func swapInts(_ a: inout Int, _ b: inout Int) {
    let temp = a; a = b; b = temp
}

func swapStrings(_ a: inout String, _ b: inout String) {
    let temp = a; a = b; b = temp
}
// Same logic, different types — duplicated code

Generic Function

Replace the concrete type with a placeholder in angle brackets <T>. Swift replaces T with the actual type at the call site.

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)")   // Output: x: 20, y: 10

var hello = "Hello"
var world = "World"
swapValues(&hello, &world)
print("\(hello) \(world)")   // Output: World Hello

Diagram: Generic Type Placeholder

func swapValues<T>(_ a: inout T, _ b: inout T)
                 ^
                 |
       T is a placeholder

Call with Int:     T becomes Int
Call with String:  T becomes String
Call with Double:  T becomes Double

One function → works for all types

Generic Types (Structs and Classes)

You can make entire types generic. Swift's own Array and Dictionary are generic types.

struct Box<T> {
    var value: T

    func describe() {
        print("Box contains: \(value)")
    }
}

let intBox = Box(value: 42)
intBox.describe()         // Output: Box contains: 42

let stringBox = Box(value: "Swift")
stringBox.describe()      // Output: Box contains: Swift

let boolBox = Box(value: true)
boolBox.describe()        // Output: Box contains: true

Generic Stack

A stack is a last-in, first-out (LIFO) collection. A generic stack works for any type.

struct Stack<Element> {
    private var items: [Element] = []

    mutating func push(_ item: Element) {
        items.append(item)
    }

    mutating func pop() -> Element? {
        return items.popLast()
    }

    var top: Element? {
        return items.last
    }

    var isEmpty: Bool {
        return items.isEmpty
    }
}

var intStack = Stack<Int>()
intStack.push(1)
intStack.push(2)
intStack.push(3)
print(intStack.top!)   // Output: 3
intStack.pop()
print(intStack.top!)   // Output: 2

Diagram: Stack Behavior (LIFO)

push(1) → [1]
push(2) → [1, 2]
push(3) → [1, 2, 3]
           ↑
          top = 3

pop()   → [1, 2]     removed: 3
           ↑
          top = 2

Type Constraints

Add constraints to limit which types a generic can accept. Use where or : notation.

func largest<T: Comparable>(_ a: T, _ b: T) -> T {
    return a > b ? a : b
}

print(largest(3, 9))          // Output: 9
print(largest("Apple", "Banana"))   // Output: Banana

T: Comparable means T must conform to Comparable. This gives the function access to the > operator.

Multiple Type Parameters

func pair<A, B>(_ first: A, _ second: B) -> String {
    return "(\(first), \(second))"
}

print(pair(1, "one"))        // Output: (1, one)
print(pair(true, 3.14))      // Output: (true, 3.14)

Generic Protocol with Associated Types

Protocols use associatedtype as their version of a generic placeholder.

protocol Container {
    associatedtype Item
    var count: Int { get }
    mutating func add(_ item: Item)
    func item(at index: Int) -> Item
}

struct Bag<T>: Container {
    private var items: [T] = []

    var count: Int { items.count }

    mutating func add(_ item: T) {
        items.append(item)
    }

    func item(at index: Int) -> T {
        return items[index]
    }
}

var bag = Bag<String>()
bag.add("Notebook")
bag.add("Pen")
print(bag.item(at: 0))   // Output: Notebook
print(bag.count)          // Output: 2

Where Clauses

A where clause adds extra requirements to a generic type or function.

func equalContainers<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.item(at: i) != c2.item(at: i) {
            return false
        }
    }
    return true
}

Opaque Types with some

The some keyword hides the specific return type while guaranteeing the protocol is met. This is common in SwiftUI.

protocol Shape {
    func area() -> Double
}

struct Square: Shape {
    var side: Double
    func area() -> Double { side * side }
}

func makeShape() -> some Shape {
    return Square(side: 4.0)
}

let s = makeShape()
print(s.area())   // Output: 16.0

Summary

Generics eliminate code duplication by replacing concrete types with placeholders like T. You can constrain generics with protocols (T: Comparable), use multiple type parameters, and build generic data structures like stacks and bags. Protocols use associatedtype for the same concept. Generics make Swift's standard library possible and are a core skill for professional Swift development.

Leave a Comment

Your email address will not be published. Required fields are marked *