Skip to content

5. Generics

What & why

A generic function or type works with any type that meets stated requirements — written once, type-checked at compile time, and (usually) as fast as hand-written specialized code. You've already used generics constantly: Array<Int>, Dictionary<String, Int>, Optional<String>.

The problem generics solve

func swapInts(_ a: inout Int, _ b: inout Int) { let t = a; a = b; b = t }
func swapStrings(_ a: inout String, _ b: inout String) { let t = a; a = b; b = t }
// ...and one for every other type? No.

Generic functions

Put a placeholder type in angle brackets after the name:

func swapValues<T>(_ a: inout T, _ b: inout T) {
    let temp = a
    a = b
    b = temp
}

var x = 1, y = 2
swapValues(&x, &y)

var s1 = "a", s2 = "b"
swapValues(&s1, &s2)

T is a stand-in; the compiler infers the real type at each call. Both parameters share T, so swapValues(&x, &s1) is rejected.

Multiple placeholders:

func pair<A, B>(_ a: A, _ b: B) -> (A, B) { (a, b) }
let p = pair(1, "one")      // (Int, String)

Constraints

An unconstrained T can do almost nothing (copy it, pass it around). Add constraints to unlock operations:

func largest<T: Comparable>(in items: [T]) -> T? {
    guard var best = items.first else { return nil }
    for item in items.dropFirst() where item > best {
        best = item
    }
    return best
}

print(largest(in: [3, 9, 2]) as Any)          // Optional(9)
print(largest(in: ["pear", "apple"]) as Any)  // Optional("pear")

Use a where clause for richer constraints:

func allEqual<C: Collection>(_ items: C) -> Bool where C.Element: Equatable {
    guard let first = items.first else { return true }
    return items.allSatisfy { $0 == first }
}
print(allEqual([2, 2, 2]), allEqual("abc"))   // true false

Combine constraints with &: T: Codable & Hashable.

Generic types

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

    mutating func push(_ item: Element) { items.append(item) }
    mutating func pop() -> Element? { items.popLast() }
    var top: Element? { items.last }
    var isEmpty: Bool { items.isEmpty }
    var count: Int { items.count }
}

var numbers = Stack<Int>()
numbers.push(1)
numbers.push(2)
print(numbers.pop() as Any)       // Optional(2)

var words = Stack<String>()       // a different type, same code
words.push("hi")

Inside the type, Element is available as a type name. For inferred cases you can omit the argument: Stack(items: ...) where it can be deduced.

Extensions with constraints

extension Stack where Element: Equatable {
    func contains(_ x: Element) -> Bool { items.contains(x) }   // needs `items` non-private or same file
}

extension Stack: CustomStringConvertible where Element: CustomStringConvertible {
    var description: String { "Stack(\(items.map(\.description).joined(separator: ", ")))" }
}

Conditional conformance is why [Int] is Equatable (because Int is) while [SomeNonEquatable] isn't.

Note

Because items is private, extensions in a different file can't see it. Extensions in the same file can. If you split files, relax to fileprivate/internal or expose a method.

Associated types

Protocols can have generic-like placeholders called associated types:

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

struct IntBox: Container {
    private var items: [Int] = []
    var count: Int { items.count }
    mutating func add(_ item: Int) { items.append(item) }     // Item inferred as Int
    subscript(i: Int) -> Int { items[i] }
}

Generic code can then require relationships:

func printAll<C: Container>(_ c: C) where C.Item: CustomStringConvertible {
    for i in 0..<c.count { print(c[i]) }
}

Protocols with associated types can't be used as plain types (let c: Container is an error). Use generics (<C: Container>), some Container, or any Container with primary associated types (Advanced lesson 2). Swift 5.7+ lets you write some Collection<Int> directly.

Opaque types: some

some Protocol means "a specific type conforming to Protocol — I won't tell you which":

func makeSquares(upTo n: Int) -> some Sequence<Int> {
    (1...n).lazy.map { $0 * $0 }
}
for s in makeSquares(upTo: 4) { print(s) }

The compiler knows the concrete type (so it can optimise), but the caller doesn't depend on it. Parameters can use it as shorthand for a generic:

func total(of values: some Sequence<Int>) -> Int { values.reduce(0, +) }
// is equivalent to:
func total2<S: Sequence>(of values: S) -> Int where S.Element == Int { values.reduce(0, +) }

Type constraints on types vs functions

Rule of thumb for modelling:

  • Many types, one algorithm → generic function with constraints.
  • Container whose element type varies → generic struct/class.
  • Behaviour that varies per type → protocol (with or without associated types).

Performance: specialization

Generics in the same module are usually specialized by the optimizer — Stack<Int> becomes code as fast as hand-written IntStack. Across module boundaries, unspecialized generic code uses runtime type metadata and can be slower. Library authors can use @inlinable or @_specialize; app authors rarely need to think about it.

Generics vs Any

func firstOf(_ a: Any, _ b: Any) -> Any { a }     // loses type info; needs casts
func firstOf<T>(_ a: T, _ b: T) -> T { a }        // type-safe, no casts

Prefer generics; they keep the compiler on your side.

Common mistakes

  • Over-generalizing. If only one type ever uses a function, write it for that type. Generalize when there's a second real use.
  • Forgetting a constraint — "binary operator '>' cannot be applied to two 'T' operands" means add : Comparable.
  • Using protocols with associated types as variable types — use generics or some/any.
  • Expecting T values of different concrete types to mix (pair<T>(_ a: T, _ b: T) with an Int and a String).
  • Making a function generic only to call a protocol method — a plain some Protocol or any Protocol parameter may be simpler.

Exercises

  1. Write a generic func countOccurrences<T: Equatable>(of x: T, in items: [T]) -> Int.
  2. Implement a generic Queue<Element> with enqueue, dequeue, peek, count.
  3. Write func zipToDictionary<K: Hashable, V>(keys: [K], values: [V]) -> [K: V].
  4. Make Stack conform to Sequence (iterate from top to bottom).
  5. Write extension Collection where Element: Numeric { func average... } — think about what return type works for Ints (hint: Double(...) isn't generic for Numeric; try BinaryInteger and FloatingPoint separate extensions).
  6. Write a Cache<Key: Hashable, Value> that stores up to N items and evicts the oldest.

Recap

  • <T> placeholders make functions and types work with any type; constraints (T: Comparable, where) unlock operations.
  • Generic types like Stack<Element> are fully type-safe and typically as fast as specialized code.
  • Conditional conformance and constrained extensions add behaviour only when it makes sense.
  • Associated types make protocols generic; some hides a concrete type while keeping it static.
  • Prefer generics over Any.

Next: Closures & higher-order functions →