Generics
This lesson builds generic functions and types, states what a type parameter must be able to do, and ends with the rule that decides between a generic and a protocol.
Generic Functions & Types
A type parameter is a placeholder written in angle brackets after the name. Inside the body it behaves like a real type: you can store it, return it, and pass it on — but you cannot assume anything the compiler has not been told.
Generic Functions
A generic function declares its type parameter in <…> right after the function name. Swift infers that parameter from the arguments, so callers never see the angle brackets at all.
// `T` stands for whatever type the caller supplies: one body, every type.
func firstOrNil<T>(_ items: [T]) -> T? {
items.isEmpty ? nil : items[0] // `T` is only stored and returned, never assumed
}
print(firstOrNil([1, 2, 3]) ?? 0) // 1 — T inferred as Int
print(firstOrNil(["a", "b"]) ?? "-") // a — T inferred as String
// The duplication this replaces: one copy per element type, identical bodies.
func firstInt(_ items: [Int]) -> Int? { items.isEmpty ? nil : items[0] }
func firstString(_ items: [String]) -> String? { items.isEmpty ? nil : items[0] }
// Inference works from the arguments; an explicit instantiation is also legal.
let numbers = [10, 20]
print(firstOrNil(numbers) ?? 0) // 10 — inferred
print(firstOrNil<Double>([1.5]) ?? 0) // 1.5 — explicit type argument
Generic Types
The same placeholder may be attached to a struct, class, or enum. Every use of the type fixes the parameter to one concrete type, so Stack<Int> and Stack<String> are two different types generated from one source.
Figure 1 — one generic definition, one concrete type per substitution; a constraint narrows which types are allowed to be substituted at all.
// A generic type stores values whose type is fixed when the type is used.
struct Stack<Element> {
private var storage: [Element] = [] // the array is generic as well
// `mutating` because Stack is a struct: push changes the value itself.
mutating func push(_ item: Element) {
storage.append(item)
}
// Returning an optional keeps "the stack may be empty" honest.
mutating func pop() -> Element? {
storage.popLast()
}
var isEmpty: Bool { storage.isEmpty }
var count: Int { storage.count }
}
var ints = Stack<Int>() // Element is now Int, everywhere in this instance
ints.push(1)
ints.push(2)
print(ints.pop() ?? 0) // 2
print(ints.count) // 1
var words = Stack<String>() // a separate type, from the same source
words.push("swift")
print(words.pop() ?? "-") // swift
// print(ints.push("no")) // error: a String is not an Int
Type Parameters & Naming
The compiler does not care about the name of a type parameter, but your reader does. Use a single capital letter for "any type", and a descriptive name once the parameter has a role in the design.
// T and U: plain "any type" parameters, used when the function is generic in
// the truest sense — it works for everything.
func swapped<T, U>(_ pair: (T, U)) -> (U, T) {
(pair.1, pair.0)
}
print(swapped((1, "one"))) // ("one", 1)
// Descriptive names describe the ROLE the value plays in the type.
struct Pair<Key, Value> {
var key: Key
var value: Value
}
let entry = Pair(key: "age", value: 30)
print(entry.value) // 30 — Value is Int here, Key is String
// Two parameters of the same name mean the SAME type — this is how a function
// states that both arguments must match.
func firstMatch<T: Equatable>(_ items: [T], lookingFor target: T) -> T? {
items.first { $0 == target } // `==` is legal only because of the constraint
}
print(firstMatch([1, 2, 3], lookingFor: 2) ?? 0) // 2
Constraints
A type parameter starts out knowing nothing. A constraint tells the compiler what it must be able to do, which is what unlocks operators, comparisons, and generic algorithms. Constraints are the price of using a capability, and they should be paid only where needed.
Stating Requirements
The shorthand form puts the requirement after a colon: <T: Numeric>. The longer where clause states a requirement after the signature, which is the only readable way to express several conditions at once.
// The constraint is what makes `+` and the starting zero legal.
func total<T: Numeric>(_ items: [T]) -> T {
items.reduce(0, +)
}
print(total([1, 2, 3])) // 6
print(total([1.5, 2.5])) // 4.0
// print(total(["a"])) // error: String does not conform to Numeric
// The same requirement written with a `where` clause.
func largest<T>(_ items: [T]) -> T? where T: Comparable {
items.max() // `>` exists because of the constraint
}
print(largest([3, 9, 4]) ?? 0) // 9
// Several conditions at once — where the `<T: ...>` form stops being readable.
// Here each element of an array must itself be a Collection of Equatable values.
func firstRepeated<C>(_ list: [C]) -> C.Element? where C: Collection, C.Element: Equatable {
for item in list {
if list.filter({ $0.contains(item) }).count > 1 { return item }
}
return nil
}
Do not add a constraint you never use. Each one removes types from the set that can call your function, so an unconstrained T is strictly more reusable — add Equatable the moment you write ==, and not one line earlier.
Associated Types
A protocol cannot have a plain type parameter, so it declares an associatedtype instead: a named role that each conforming type fills in with a concrete type. Inside the protocol, that role behaves like any other type.
// `Item` is a role, not a concrete type: the conforming type decides.
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(index: Int) -> Item { get }
}
// A generic type fills the role with its own type parameter.
struct Stack<Element>: Container {
private var storage: [Element] = []
mutating func append(_ item: Element) { storage.append(item) }
var count: Int { storage.count }
subscript(index: Int) -> Element { storage[index] }
}
// A generic function can now accept ANY container and still know the element
// type: `C.Item` is spelled out at the call site's type.
func firstItem<C: Container>(_ container: C) -> C.Item? {
container.count > 0 ? container[0] : nil
}
var stack = Stack<Int>()
stack.append(7)
print(firstItem(stack) ?? 0) // 7 — Item was inferred as Int from Element
// Arrays already conform to an equivalent protocol, so the same function works
// on them without any change.
print(firstItem([1, 2, 3]) ?? 0) // 1
Same-Type Constraints
Conformance says "must be able to". A same-type constraint says "must be exactly this, and the same as that other one". Written with ==, it ties two type parameters together and is the tool for functions that combine two different generic types.
// A.Item == B.Item requires the two containers to hold the SAME element type,
// while the container types themselves are free to differ.
func copies<A, B>(_ source: A, into destination: B) -> Bool
where A: Container, B: Container, A.Item == B.Item {
source.count <= destination.count // only counts are compared here
}
var stack = Stack<Int>()
stack.append(1)
print(copies(stack, into: [1, 2, 3])) // true — Stack<Int>.Item == Int
// print(copies(stack, into: ["a"])) // error: Int is not String
// Inside an extension, `Self` may carry the constraint instead.
extension Stack where Self: Container, Element: Equatable {
func contains(_ element: Element) -> Bool {
storage.contains(element) // `==` is available for Element now
}
}
print(stack.contains(1)) // true
Extending Generic Types
Generic types are extended like any other, but the extension may declare conditions. That is how a generic type gains a capability only for the substitutions where the capability makes sense — the feature the standard library uses to make arrays of equatable elements equatable.
Conditional Conformance
A conditional extension adds a protocol conformance that exists only when the type parameter satisfies a further condition. The conformance appears and disappears with the substitution, so unsupported cases simply do not compile.
// The extension lives in the same file as Stack, so the private storage is
// reachable; that is the usual arrangement for a generic type and its extensions.
extension Stack: Equatable where Element: Equatable {
static func == (lhs: Stack<Element>, rhs: Stack<Element>) -> Bool {
lhs.storage == rhs.storage // arrays of Equatable compare element-wise
}
}
var a = Stack<Int>()
a.push(1)
var b = Stack<Int>()
b.push(1)
print(a == b) // true
// Stack of a non-Equatable element has no `==` at all: the conformance is
// simply absent, and the compiler says so at the call site.
// final class Blob { }
// Stack<Blob>() == Stack<Blob>() // error: no `==` for this substitution
Specialized Extensions
An extension may also add members for one particular substitution. The capability exists only where it is meaningful, which keeps the generic type small without giving up convenience.
// Extra members only where the element can be ordered.
extension Stack where Element: Comparable {
var maximum: Element? { storage.max() }
}
// Extra members for one concrete substitution — occasionally worth its weight.
extension Stack where Element == String {
func joined(separator: String = " ") -> String {
storage.joined(separator: separator)
}
}
var numbers = Stack<Int>()
numbers.push(3)
numbers.push(9)
print(numbers.maximum ?? 0) // 9
// Stack<Bool>() has no `maximum`: Bool is not Comparable
var words = Stack<String>()
words.push("swift")
words.push("is")
words.push("fast")
print(words.joined()) // swift is fast
Generics in Practice
You now have four ways to write code that works with more than one type. They are not interchangeable: each one trades flexibility against cost in a different way, and the table is the shortest summary of that trade.
Generic, any, or Concrete?
Approach What it costs Use it when
Concrete type
Nothing — the simplest code to read and debug.
Only one type will ever be used.
Generic parameter <T: P>
No box; one specialised implementation per type, so build time and code size grow.
The algorithm is identical for every conforming type.
any P (existential)
A box and a witness-table lookup per call; the value is copied into the box.
Values of several conforming types must be stored together.
some P (opaque)
No box, but the type is fixed once and hidden from callers.
A function returns one internal type you prefer not to expose.
Start concrete. Move to a generic parameter when a second type appears. Reach for any only when you genuinely must mix types in one collection or one stored property.
Common Pitfalls
// 1. An unconstrained type parameter supports no operations. You cannot add,
// compare, or print it — add the smallest constraint that enables the code.
// 2. Every constraint shrinks the set of types that can call the function.
// Write `==` before you write `Equatable`, never the other way around.
// 3. `some` names exactly ONE concrete type, so two return paths must agree.
// Use `any` when the concrete type genuinely varies at run time.
// 4. A generic type cannot be used with its parameters unfixed:
// var stack = Stack() // error: generic parameter 'Element' not inferred
var stack: Stack<Int> = Stack() // fine: the annotation supplies the type
// 5. Prefer a generic parameter to `Any` plus a downcast: `Any` discards type
// safety, while a generic keeps it and is usually just as fast.
Practice: write a generic minMax<T: Comparable>(_:) that returns both the smallest and the largest element of an array as a labelled tuple. Then add a conditional extension so your Stack conforms to CustomStringConvertible only when Element does, and check that print(stack) works for Stack<Int> but not for a stack of plain classes.
Generics complete the type system: value and reference types, enums, protocols, and type parameters. Phase 4 turns to the runtime — Memory Management explains how ARC keeps class instances alive, which is what decides when a deinit finally runs.