Protocol extensions and generics
Where protocols gain real behaviour, plus the generic machinery that lets one function work with many types at no runtime cost.
By the end you will be able to
- Give protocols default implementations and explain when they are used
- Write generic functions and types with constraints
- Choose between a generic parameter and an existential
Collection has dozens of methods — map, filter, first, dropFirst, enumerated — yet a type only needs to supply startIndex, endIndex, index(after:) and a subscript to get all of them. How?
Default implementations
protocol Greeter {
var name: String { get }
func greet() -> String
}
extension Greeter {
func greet() -> String {
"Hello, \(name)!"
}
}
struct Person: Greeter {
var name: String
}
Person never implements greet() and gets it anyway. A conforming type can override the default by providing its own.
Notice shout() — defined only in the extension, in terms of greet(). Each type gets it without writing anything, and it correctly picks up the overridden greet for the pirate.
Constrained extensions
An extension can apply only where a condition holds:
extension Collection where Element: Numeric {
var total: Element {
reduce(0, +)
}
}
[1, 2, 3].total // 6
["a", "b"].total // error: no such member
This is how the standard library gives [Int] a sum-like capability without polluting [String]. Constrained extensions are one of Swift's sharpest tools: behaviour appears exactly where it makes sense and nowhere else.
Generics: one implementation, many types
A generic function keeps the caller's concrete type instead of erasing it:
func firstOrNil<T>(_ items: [T]) -> T? {
items.isEmpty ? nil : items[0]
}
firstOrNil([1, 2, 3]) // Int?
firstOrNil(["a"]) // String?
T is a placeholder filled in at each call site. Two things are guaranteed: the concrete type is preserved, and there is no existential box. On top of that, when the optimiser can see the function's body (same module with optimisation on, or @inlinable), it generates specialised code per concrete type and can inline the calls — without that visibility, generic code still routes protocol requirements through a small runtime lookup table. Specialisation is an optimisation you usually get, not a language guarantee.
Constraints
An unconstrained T can do almost nothing — the compiler knows nothing about it. Constraints add capability:
func largest<T: Comparable>(_ items: [T]) -> T? {
items.max()
}
func describe<T: CustomStringConvertible>(_ value: T) -> String {
"value: \(value.description)"
}
where clauses handle more elaborate conditions:
func merge<C: Collection>(_ collection: C) -> String where C.Element == String {
collection.joined(separator: ", ")
}Generic types
struct Stack<Element> {
private var items: [Element] = []
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element? {
items.popLast()
}
var count: Int { items.count }
var isEmpty: Bool { items.isEmpty }
}
var numbers = Stack<Int>()
var words = Stack<String>()
Array, Dictionary, Set and Optional are all generic types written exactly this way.
Generic or existential?
Both let one function work with several types. They are not interchangeable.
func process<T: Shape>(_ shape: T) {
// T is one concrete type here.
// Compiler specialises, inlines,
// no boxing.
}
// Cannot hold mixed types:
// let mixed: [T] — nonsensefunc process(_ shape: any Shape) {
// Concrete type unknown.
// Dynamic dispatch, possible boxing.
}
// Can hold mixed types:
let mixed: [any Shape] = [
Circle(), Square()
]The rule: generic by default. Reach for any only when you genuinely need a heterogeneous collection or must store the value without knowing its type. A generic keeps performance and type information; an existential trades both for flexibility you often do not need.
A generic constrained to Numeric behaves differently for Int and Double. What does this print?
func total<T: Numeric>(_ values: [T]) -> T {
values.reduce(0, +)
}
print(total([1, 2, 3]))
print(total([1.5, 2.5]))
print(total([Int]()))Associated types
A protocol can have a placeholder type of its own:
protocol Container {
associatedtype Item
var count: Int { get }
mutating func append(_ item: Item)
subscript(index: Int) -> Item { get }
}
struct IntStack: Container {
var items: [Int] = []
var count: Int { items.count }
mutating func append(_ item: Int) { items.append(item) }
subscript(index: Int) -> Int { items[index] }
}
Item is inferred from the implementation — here, Int. This is how Collection declares Element and Iterator.
Associated types are also why some protocols cannot be used as plain existentials without care: any Container does not say what it contains. Swift 5.7 made this workable with primary associated types (any Container<Int>), but generics remain the cleaner answer.
When should you prefer func handle<T: Shape>(_ s: T) over func handle(_ s: any Shape)?
Default implementation
Give Identifiable2 a default shortID that returns the first four characters of id, and let one type override it.
Write a generic function
Write pairs(_:_:) that zips two arrays into an array of tuples, stopping at the shorter one — for any two element types.
Constrain an extension
Add a total property to arrays of Int only — ["a"] must not gain it.
Explain to someone who knows Java interfaces what a Swift protocol extension adds that a Java interface's default methods do not. Focus on constrained extensions and on retroactive conformance.
You can now:
- Supply default implementations in protocol extensions and know when they apply
- Constrain an extension so behaviour appears only where it makes sense
- Write generic functions and types with constraints
- Choose a generic over an existential, and say why
Next up: the protocols Swift synthesises for you — Equatable, Hashable, Comparable and Codable.