05 · Generics¶
You've already been using generics without necessarily calling them that —
Array<Element>, Optional<Wrapped>, and Dictionary<Key, Value> are all
generic types from the standard library. Generics let you write one function
or type that works with any type, while still keeping full type safety —
no casting, no Any, no runtime surprises.
The problem generics solve¶
Without generics, supporting multiple types means duplicating code:
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
}
The logic is identical — only the type differs. A generic function expresses that once:
func swapValues<T>(_ a: inout T, _ b: inout T) {
let temp = a
a = b
b = temp
}
var x = 1, y = 2
swapValues(&x, &y)
print(x, y) // 2 1
var name1 = "Alice", name2 = "Bob"
swapValues(&name1, &name2)
print(name1, name2) // Bob Alice
T is a type parameter — a placeholder the compiler fills in with a
concrete type at each call site. swapValues(&x, &y) compiles as if you'd
written swapValues<Int>, and swapValues(&name1, &name2) as if you'd
written swapValues<String> — both fully type-checked, with zero runtime
overhead for the abstraction.
Generic functions with return values¶
func firstAndLast<T>(_ items: [T]) -> (first: T, last: T)? {
guard let first = items.first, let last = items.last else {
return nil
}
return (first, last)
}
if let result = firstAndLast([10, 20, 30]) {
print(result.first, result.last) // 10 30
}
if let result = firstAndLast(["x", "y", "z"]) {
print(result.first, result.last) // x z
}
print(firstAndLast([Int]()) == nil) // true -- empty array
Type constraints¶
Sometimes T can be anything, but sometimes you need T to support a
specific capability — like being comparable with <. A constraint
requires T to conform to a protocol:
func largest<T: Comparable>(_ items: [T]) -> T? {
guard var current = items.first else { return nil }
for item in items.dropFirst() where item > current {
current = item
}
return current
}
print(largest([3, 7, 2, 9, 4]) ?? "empty") // 9
print(largest(["pear", "apple", "kiwi"]) ?? "empty") // pear
Without T: Comparable, the line item > current wouldn't compile — the
compiler has no way to know an arbitrary T supports >. The constraint is
what makes > legal, by guaranteeing every T passed in has it.
Generic types¶
Types, not just functions, can be generic — this is how you'd build your
own Stack, Queue, or Box:
struct Stack<Element> {
private var items: [Element] = []
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element? {
items.popLast()
}
var isEmpty: Bool { items.isEmpty }
var count: Int { items.count }
}
var intStack = Stack<Int>()
intStack.push(1)
intStack.push(2)
intStack.push(3)
print(intStack.pop() ?? -1) // 3
print(intStack.count) // 2
var stringStack = Stack<String>()
stringStack.push("first")
stringStack.push("second")
print(stringStack.pop() ?? "empty") // second
Stack<Int> and Stack<String> are different specializations of the
same generic type — the compiler generates type-safe code for each, so
intStack.push("oops") is a compile error, not a runtime one.
Multiple constraints and where clauses¶
You can require more than one capability, and use where for more complex
constraints, including constraints on associated types:
func printSortedUnique<T: Comparable & Hashable>(_ items: [T]) {
let unique = Set(items) // needs Hashable
let sorted = unique.sorted() // needs Comparable
print(sorted)
}
printSortedUnique([3, 1, 2, 3, 1]) // [1, 2, 3]
func allElementsMatch<C1: Collection, C2: Collection>(_ first: C1, _ second: C2) -> Bool
where C1.Element == C2.Element, C1.Element: Equatable {
guard first.count == second.count else { return false }
return zip(first, second).allSatisfy { $0 == $1 }
}
print(allElementsMatch([1, 2, 3], [1, 2, 3])) // true
print(allElementsMatch([1, 2, 3], Set([3, 2, 1]))) // true -- Array and Set, same elements
allElementsMatch accepts two different collection types (Array and
Set in the second call) as long as they hold the same, Equatable
element type — generics compose with protocols to express exactly the
requirement you need, no more and no less.
Associated types: generics in protocols¶
A protocol can declare a placeholder type using associatedtype — the
conforming type fills it in, similar to how a generic function's caller
fills in T:
protocol Container {
associatedtype Item
mutating func add(_ item: Item)
var count: Int { get }
}
struct IntBag: Container {
private var items: [Int] = []
mutating func add(_ item: Int) { items.append(item) } // Item is inferred as Int
var count: Int { items.count }
}
var bag = IntBag()
bag.add(5)
bag.add(10)
print(bag.count) // 2
This is exactly how the standard library's own Sequence and Collection
protocols work under the hood — associatedtype Element is why for x in
myArray and for x in mySet both work with fully type-safe x.
Cheat sheet¶
| Concept | Syntax | Purpose |
|---|---|---|
| Generic function | func f<T>(_ x: T) |
One implementation, many concrete types |
| Type constraint | <T: Comparable> |
Require a capability so the body can use it |
| Multiple constraints | <T: Comparable & Hashable> |
Require more than one capability |
where clause |
where C1.Element == C2.Element |
Constrain relationships between type parameters |
| Generic type | struct Stack<Element> { ... } |
A reusable container/type, specialized per use |
associatedtype |
protocol Container { associatedtype Item } |
A placeholder type inside a protocol |
How It Actually Works¶
Generics in Swift are resolved through witness tables, not type
erasure. When you call swapValues(&x, &y) with Int, the compiler
knows the concrete type at the call site and, in most cases, specializes
the generic function — generating a dedicated machine-code copy of
swapValues for Int at compile time (visible with whole-module
optimization), identical in performance to hand-writing swapInts. This is
why generics carry "zero runtime overhead for the abstraction": the
abstraction is erased by the optimizer, not preserved and dispatched at
runtime, in contrast to type-erased Any-based approaches.
Protocol constraints compile to a witness table, not a vtable. <T:
Comparable> doesn't give the function a class-style vtable pointer;
instead, when full specialization isn't possible (a function in a
separately-compiled module, called with a type not known until runtime),
the compiler passes a hidden value witness table and protocol witness
table alongside T — essentially a struct of function pointers for T's
size/alignment/copy behavior and its Comparable conformance (<, ==,
etc.). The generic function calls through those pointers instead of
static dispatch. This indirection is what lets one compiled largest<T:
Comparable> binary work across every Comparable type ever defined,
including ones written after the generic function was compiled.
associatedtype is resolved the same way, one level up. A protocol
with an associatedtype can't be used as a standalone type (Container
alone isn't a valid type annotation) because the compiler needs to know
Item's witness table to lay out calls through it — this is precisely why
you write some Container or a generic <C: Container> constraint
instead: both give the compiler a concrete Item to resolve at the call
site, whereas a bare existential wouldn't carry the associated-type
information needed to generate a witness table for it (Swift's any
Container existential support for this is deliberately restricted for
exactly this reason).
Why Stack<Int> and Stack<String> are called "specializations":
each instantiation gets its own independently laid-out storage (an [Int]
buffer vs. a [String] buffer, per the copy-on-write array mechanism), so
there is no shared "generic Stack" object at runtime — only concrete,
type-specific instances that happen to share one source definition.
Exercise¶
Write a generic function func average<T: BinaryInteger>(_ values: [T]) ->
Double that returns the arithmetic mean as a Double (hint: convert each
element with Double(value)), returning 0 for an empty array. Then define
a generic struct Pair<A, B> holding a first: A and second: B, with a
method swapped() -> Pair<B, A> that returns a new pair with the elements
reversed. Create a Pair<String, Int>, print it, call swapped(), and print
the result to confirm the types flipped correctly.