Lesson 14 / 25
Generics, Associated Types, some and any
Write generic code and choose between opaque types and existentials.
Code that works with many types
Generics let functions and types work with any type that meets requirements, while keeping full type safety: func largest<T: Comparable>(_ items: [T]) -> T?. Constraints use protocols (T: Hashable) and where clauses for more complex rules. Generic types such as Stack<Element> and Swift's own Array<Element> and Dictionary<Key, Value> are specialised by the compiler for performance. Protocols can declare associated types, placeholders filled in by conforming types (Sequence has an Element). Two keywords control how protocol types are used. some Protocol is an opaque type: one specific concrete type that the compiler knows but callers do not, with no runtime cost, used everywhere in SwiftUI (var body: some View). any Protocol is an existential: a box that can hold any conforming type and can change at run time, needed for heterogeneous collections ([any Shape]) but with some dynamic dispatch cost and restrictions. Prefer generics or some by default and use any when you truly need different types mixed together.
Generics, constraints and some versus any
One generic function, one generic type and both protocol-type forms.
func largest<T: Comparable>(_ items: [T]) -> T? {
guard var best = items.first else { return nil }
for item in items.dropFirst() where item > best { best = item }
return best
}
largest([3, 9, 4]) // Optional(9)
largest(["pear", "apple"]) // Optional("pear")
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 }
}
protocol Shape { func area() -> Double }
struct Circle: Shape { let r: Double; func area() -> Double { .pi * r * r } }
struct Square: Shape { let side: Double; func area() -> Double { side * side } }
func makeDefaultShape() -> some Shape { Circle(r: 1) } // opaque: one hidden concrete type
let mixed: [any Shape] = [Circle(r: 1), Square(side: 2)] // existential: different types together
let totalArea = mixed.reduce(0) { $0 + $1.area() }
func printAreas(_ shapes: some Collection<any Shape>) { // primary associated type
for s in shapes { print(s.area()) }
}A sealed gift versus a mixed hamper
some Shape is a sealed gift: there is exactly one thing inside and the giver knows what, but you only know it is a shape. any Shape is a hamper that can hold a different shape in every slot, which is flexible but needs more handling.
Quick check: When do you need `any Protocol` rather than `some Protocol`?
- Never
- When returning from SwiftUI body
- When using generics with constraints
- When values of different concrete types must be stored together, such as in [any Shape]
Answer
When values of different concrete types must be stored together, such as in [any Shape] — Existentials allow heterogeneous values; opaque types stand for one fixed concrete type.