Generics and constraints

Swift · Practical · 12 min

Generics let one implementation work for many types with full type safety, as Array and Dictionary do.

A generic function has type parameters in angle brackets: func first<T>(_ xs: [T]) -> T?. The compiler infers T at each call and checks types as if you'd written a specific version.

Constraints say what a type parameter must support: func largest<T: Comparable>(_ xs: [T]) -> T? can use <. Longer requirements go in a where clause: where T: Hashable, U.Element == T.

Generic types work the same way: struct Stack<Element> { private var items: [Element] = [] }. Extensions can add members for specific constraints: extension Stack where Element: Equatable.

Protocols use associated types instead of angle brackets: Collection has associatedtype Element. A conforming type fixes it, often by inference. You constrain associated types in generic code: func sum<C: Collection>(_ c: C) -> Int where C.Element == Int. Swift 5.7 added primary associated types, so you can write some Collection<Int> or any Sequence<String> directly.

The shorthand func process(_ items: some Collection<Int>) is equivalent to a generic parameter — it is the same as writing <C: Collection<Int>>.

Generics are specialized by the optimizer when the concrete type is visible, so they are usually as fast as hand-written code; across module boundaries they may use a slower dictionary-passing path unless the function is @inlinable.

Example

struct Stack<Element> {
    private var items: [Element] = []
    mutating func push(_ x: Element) { items.append(x) }
    mutating func pop() -> Element? { items.popLast() }
    var top: Element? { items.last }
}
extension Stack where Element: Numeric {
    var total: Element { items.reduce(0, +) }
}

func largest<T: Comparable>(_ xs: [T]) -> T? {
    guard var best = xs.first else { return nil }
    for x in xs where x > best { best = x }
    return best
}

func countMatches(in items: some Sequence<String>, prefix: String) -> Int {
    items.filter { $0.hasPrefix(prefix) }.count
}

var s = Stack<Int>()
s.push(3); s.push(4)
print(s.top ?? 0, s.total)                        // 4 7
print(largest(["pear", "apple", "zebra"]) ?? "")  // zebra
print(countMatches(in: ["swift", "sql", "go"], prefix: "s"))  // 2

Remember

  • Type parameters plus constraints give reusable, type-safe code
  • Protocols use associated types; constrain them in where clauses
  • some P<T> in a parameter is shorthand for a generic parameter

Watch out for

  • Using [Any] or any P where a generic would keep full type information.

Check yourself

  1. What does the constraint in `func f<T: Hashable>(_ x: T)` guarantee inside f?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.swift.org/swift-book/documentation/the-swift-programming-language/generics/