Reading — step 1 of 7
Learn
Swift's generics let you write code that works with any type — but with constraints that ensure the type has the capabilities you need. Apple's Swift Programming Language book covers generics across two chapters; this lesson focuses on constraints and where clauses, which separate competent from advanced Swift code.
Basic generics (review)
func swap<T>(_ a: inout T, _ b: inout T) {
let tmp = a
a = b
b = tmp
}
var x = 1, y = 2
swap(&x, &y)
T is a type parameter. The compiler instantiates the function for each type used at the call site (monomorphization).
Type constraints with where
Constrain T to types that conform to a protocol:
func largest<T: Comparable>(_ items: [T]) -> T? {
guard let first = items.first else { return nil }
return items.dropFirst().reduce(first) { $0 > $1 ? $0 : $1 }
}
T: Comparable means "T must conform to Comparable" — gives you <, >, etc.
Multiple constraints with & and where:
func allEqual<S: Sequence>(_ seq: S) -> Bool
where S.Element: Equatable {
var iter = seq.makeIterator()
guard let first = iter.next() else { return true }
while let next = iter.next() {
if next != first { return false }
}
return true
}
allEqual([1, 1, 1]) // true
allEqual([1, 2, 1]) // false
The where S.Element: Equatable constrains the associated type of S — Sequences whose elements support ==.
Protocols with associated types
A protocol can declare placeholder types that conformers fill in:
protocol Container {
associatedtype Item
var count: Int { get }
mutating func append(_ item: Item)
subscript(i: Int) -> Item { get }
}
struct Stack<Element>: Container {
typealias Item = Element
private var items: [Element] = []
var count: Int { items.count }
mutating func append(_ item: Element) { items.append(item) }
subscript(i: Int) -> Element { items[i] }
}
The Item is associated — each conformer picks one. Compile-time bound.
Generic extensions with where
extension Array where Element: Numeric {
func sum() -> Element {
return reduce(0, +)
}
}
[1, 2, 3].sum() // 6 — only works on numeric arrays
["a", "b"].sum() // ERROR — String isn't Numeric
This lets you add methods to a generic type ONLY when its parameter satisfies a constraint. Powerful for type-specific APIs without subclassing.
Conditional conformance
struct Pair<A, B> {
let first: A
let second: B
}
extension Pair: Equatable where A: Equatable, B: Equatable {
static func == (lhs: Pair, rhs: Pair) -> Bool {
return lhs.first == rhs.first && lhs.second == rhs.second
}
}
Pair is Equatable IF both its components are Equatable. Same idea drives Array: Equatable where Element: Equatable in the standard library.
Same-type constraints
The most powerful where clause — equate two associated types:
func combineSequences<S1: Sequence, S2: Sequence>(
_ a: S1, _ b: S2
) -> [S1.Element]
where S1.Element == S2.Element {
return Array(a) + Array(b)
}
Forces both sequences to have the same element type. Without this, you couldn't combine them safely.
Common mistakes
- Constraining with class hierarchies when a protocol would do — protocols are the Swift way.
- Not using
wherefor clarity — long inline constraints can make signatures unreadable. Move towhereclauses. - Forgetting
associatedtypefor protocols with element types — the protocol becomes useless without it. - Trying to use
==between two generic types without constraint — the compiler doesn't know they're Equatable. AddT: Equatable. - Excessive constraints — sometimes only one method needs the constraint. Use a where clause on the specific method, not the whole type.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…