iosNudge

What are generics in Swift, and how do constraints work?

MediumAsked oftenSwiftGenericsProtocols 2 min read

TL;DR

Generics let you write code once and use it with many types, while keeping full type safety. A constraint says what the type must be able to do, like T: Comparable. Use where clauses for more complex rules. Array, Dictionary and Optional are all generic.

A generic function

func swapPair<T>(_ pair: (T, T)) -> (T, T) {
    (pair.1, pair.0)
}

swapPair((1, 2))          // (2, 1)
swapPair(("a", "b"))      // ("b", "a")

T is a type parameter. Swift infers it from the arguments. Both values must be the same type.

Adding a constraint

Without a constraint, you can do almost nothing with T. You cannot compare it or add it.

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

largest([3, 9, 2])          // 9
largest(["pear", "apple"])  // "pear"

T: Comparable lets you use >.

A generic type

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 numbers = Stack<Int>()
numbers.push(1)

var words = Stack<String>()
words.push("hi")

where clauses

Use where when you need more than one rule, or rules about associated types.

func allEqual<C: Collection>(_ items: C) -> Bool where C.Element: Equatable {
    guard let first = items.first else { return true }
    return items.allSatisfy { $0 == first }
}

func merge<A: Sequence, B: Sequence>(_ a: A, _ b: B) -> [A.Element]
    where A.Element == B.Element {
    Array(a) + Array(b)
}

Constrained extensions

Add methods only when the generic type meets a condition.

extension Stack where Element: Numeric {
    func sum() -> Element { items.reduce(0, +) }   // same file, so private items is visible
}

Conditional conformance

A generic type conforms to a protocol only when its parameter does.

extension Stack: Equatable where Element: Equatable { }

// Stack<Int> is Equatable. Stack<UIView> is not.

This is why [Int] is Equatable but [Any] is not.

Short syntax with some

Since Swift 5.7, some in a parameter is a shorter generic.

func printAll(_ items: some Collection<String>) {
    for item in items { print(item) }
}

// Same as:
func printAll<C: Collection>(_ items: C) where C.Element == String { }

Use the long form when two parameters must share one type.

Generics vs Any vs any Protocol

Generic Tany ProtocolAny
Type known at compile timeYesNoNo
Needs castingNoSometimesYes
SpeedFastest (can specialize)Slower (box + dynamic dispatch)Slowest
Mixed types in one arrayNoYesYes

How generics run

Swift compiles a generic function once and passes type information at run time. When the optimizer can see the concrete type, it specializes the function: it creates a version just for Int, for example. Then calls are as fast as normal code.

@inlinable lets other modules specialize your generic code. It is used in libraries like the standard library.

Common mistakes

  • Using Any instead of a generic. You lose type safety and need casts.
  • Over-generic code. If only one type uses it, a plain type is clearer.
  • Forgetting that T is fixed per call. largest([1, "a"]) does not compile.

Common follow-up questions

  • What is the difference between a generic and an associated type? The caller picks a generic parameter. The conforming type picks an associated type.
  • What are parameter packs? Swift 5.9 added each T and repeat to write functions that take any number of different types, like func all<each T>(_ v: repeat each T).
  • Can generics work with noncopyable types? Yes, since Swift 6, with T: ~Copyable. By default every generic parameter is assumed copyable.
  • What is a generic constraint on a class? T: UIView means T must be UIView or a subclass. T: AnyObject means any class.
  • Why do some generic errors look so long? The compiler tries many overloads during type inference. Adding explicit types often gives clearer errors and faster builds.