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:
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
Tvalues of different concrete types to mix (pair<T>(_ a: T, _ b: T)with anIntand aString). - Making a function generic only to call a protocol method — a plain
some Protocolorany Protocolparameter may be simpler.
Exercises¶
- Write a generic
func countOccurrences<T: Equatable>(of x: T, in items: [T]) -> Int. - Implement a generic
Queue<Element>withenqueue,dequeue,peek,count. - Write
func zipToDictionary<K: Hashable, V>(keys: [K], values: [V]) -> [K: V]. - Make
Stackconform toSequence(iterate from top to bottom). - Write
extension Collection where Element: Numeric { func average... }— think about what return type works forInts (hint:Double(...)isn't generic forNumeric; tryBinaryIntegerandFloatingPointseparate extensions). - 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;
somehides a concrete type while keeping it static. - Prefer generics over
Any.