Swift: Swift Generics Tutorial
Generics let you write flexible code that works with any type, avoiding the need to duplicate the same logic for each type -- like a universal mold that produces the right shape no matter what material you pour in.
1. What You'll Learn
- How to define generic functions and generic types
- How type constraints limit the scope of generic parameters
- Associated types in protocols
- Advanced usage of where clauses
- Real-world applications of generics in the standard library
2. A Backend Engineer's Real Story
(1) Pain Point: Writing the Same Logic for Every Type
Charlie was building a caching service and needed a last-in-first-out stack data structure. He first wrote a version for Int:
struct IntStack {
private var items: [Int] = []
mutating func push(_ item: Int) { items.append(item) }
mutating func pop() -> Int? { items.popLast() }
}
But soon he needed to support String, Double, and even a custom User type. Charlie found himself copying and pasting -- only the type name changed; the logic was identical.
(2) The Generic Solution
Generics turn the type into a parameter -- one implementation fits all:
struct Stack<Element> {
private var items: [Element] = []
mutating func push(_ item: Element) { items.append(item) }
mutating func pop() -> Element? { items.popLast() }
}
Now Stack<Int>, Stack<String>, and Stack<User> all use the same implementation, with the compiler ensuring type safety.
(3) Benefits: 60% Less Code
| Dimension | Non-Generic Approach | Generic Approach |
|---|---|---|
| Lines of code | 120 lines (3 types) | 20 lines (1 generic) |
| Cost of adding a new type | 40 lines of copy-paste | 1 line: Stack<NewType> |
| Type safety | Each type independent | Compile-time checked |
| Maintenance cost | Change logic in 3 places | Change only in 1 place |
3. Generic Functions
Generic functions let you use placeholder types in function definitions; the concrete type is determined at the call site.
graph LR
A["func swap<T>(a: inout T, b: inout T)"] --> B["Called with Int"]
A --> C["Called with String"]
A --> D["Called with Double"]
B --> E["T = Int: safe swap"]
C --> F["T = String: safe swap"]
D --> G["T = Double: safe swap"]
(1) Generic Parameter Syntax
Generic parameters are written in angle brackets <> after the function name, conventionally using uppercase letters T, U, V as placeholders.
| Syntax | Meaning | Example |
|---|---|---|
<T> |
Single generic parameter | func identity<T>(_ value: T) -> T |
<T, U> |
Two generic parameters | func pair<T, U>(_ a: T, _ b: U) -> (T, U) |
<T: Equatable> |
Constrained generic | func isEqual<T: Equatable>(_ a: T, _ b: T) -> Bool |
▶ Example: Swapping Two Variable Values
// ============================================
// Generic function: swap two values of any type
// ============================================
func swapValues<T>(_ a: inout T, _ b: inout T) {
let temp = a
a = b
b = temp
}
var x = 10
var y = 20
swapValues(&x, &y)
print("x = \(x), y = \(y)")
var firstName = "Alice"
var lastName = "Bob"
swapValues(&firstName, &lastName)
print("firstName = \(firstName), lastName = \(lastName)")
Output:
TEXT 📖 Display onlyx = 20, y = 10 firstName = Bob, lastName = Alice
(2) Multiple Generic Parameters
Functions can have multiple generic parameters, each representing a different type at a different position.
▶ Example: Building a Key-Value Pair
// ============================================
// Multiple generic parameters to build a key-value pair
// ============================================
func makePair<K, V>(_ key: K, _ value: V) -> (K, V) {
return (key, value)
}
let pair1 = makePair("id", 1001)
let pair2 = makePair(3.14, "Pi")
print(pair1)
print(pair2)
Output:
TEXT 📖 Display only(id, 1001) (3.14, Pi)
4. Generic Types
Generic types let you define structs, classes, or enums with placeholders, enabling them to handle data of any type.
graph TB
A["Stack<Element>"] --> B["push(Element)"]
A --> C["pop() -> Element?"]
A --> D["peek() -> Element?"]
A --> E["count: Int"]
B --> F["append to items"]
C --> G["remove last from items"]
D --> H["return items.last"]
(1) Generic Structs
struct Stack<Element> { ... }
| Use Case | Syntax | Description |
|---|---|---|
| Int stack | Stack<Int> |
Can only push/pop Int values |
| String stack | Stack<String> |
Can only push/pop String values |
| Custom type stack | Stack<User> |
Can only push/pop User values |
▶ Example: Generic Stack Data Structure
// ============================================
// Generic stack: supports last-in-first-out for any type
// ============================================
struct Stack<Element> {
private var items: [Element] = []
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element? {
items.popLast()
}
func peek() -> Element? {
items.last
}
var count: Int { items.count }
}
var intStack = Stack<Int>()
intStack.push(10)
intStack.push(20)
intStack.push(30)
print("Pop: \(intStack.pop() ?? 0)")
var stringStack = Stack<String>()
stringStack.push("Alice")
stringStack.push("Bob")
print("Peek: \(stringStack.peek() ?? "")")
Output:
TEXT 📖 Display onlyPop: 30 Peek: Bob
(2) Generic Extensions
When extending a generic type, you don't need to redeclare the type parameter -- just use the placeholder name directly.
▶ Example: Adding a map Method to the Generic Stack
// ============================================
// Adding map functionality to Stack via extension
// ============================================
extension Stack {
func map<T>(_ transform: (Element) -> T) -> Stack<T> {
var result = Stack<T>()
for item in items {
result.push(transform(item))
}
return result
}
}
var s = Stack<Int>()
s.push(1); s.push(2); s.push(3)
let doubled = s.map { $0 * 2 }
print("Count: \(doubled.count)")
Output:
TEXT 📖 Display onlyCount: 3
5. Type Constraints and Associated Types
Type constraints restrict generic parameters to satisfy specific conditions. Associated types are "generic placeholders" in protocols, specified by the implementer.
graph TB
A["<T: Equatable>"] --> B["Can use =="]
A --> C["func findIndex<T: Equatable>(of: T, in: [T]) -> Int?"]
B --> D["Int =="]
B --> E["String =="]
B --> F["Custom with Equatable"]
(1) Type Constraints
| Constraint Syntax | Meaning | Use Case |
|---|---|---|
<T: Equatable> |
T must be equatable | Finding elements, deduplication |
<T: Hashable> |
T must be hashable | Dictionary keys |
<T: Comparable> |
T must be comparable | Sorting, finding min/max |
<T: Codable> |
T must be codable/decodable | JSON serialization |
▶ Example: Using Equatable Constraint to Find an Element
// ============================================
// Constraining generic parameter to be equatable
// ============================================
func findIndex<T: Equatable>(of value: T, in array: [T]) -> Int? {
for (index, item) in array.enumerated() {
if item == value {
return index
}
}
return nil
}
let numbers = [10, 20, 30, 40]
if let index = findIndex(of: 30, in: numbers) {
print("Found at index \(index)")
}
let names = ["Alice", "Bob", "Charlie"]
if let index = findIndex(of: "Bob", in: names) {
print("Found at index \(index)")
}
Output:
TEXT 📖 Display onlyFound at index 2 Found at index 1
(2) Associated Types and where Clauses
The associatedtype keyword lets protocols support generics -- the protocol itself doesn't specify a concrete type; the implementer decides.
▶ Example: Protocol with Associated Type and where Clause
// ============================================
// Associated type protocol + where clause
// ============================================
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
struct Box<T>: Container {
typealias Item = T
private var items: [T] = []
mutating func append(_ item: T) { items.append(item) }
var count: Int { items.count }
subscript(i: Int) -> T { items[i] }
}
func allItemsMatch<C1: Container, C2: Container>(
_ c1: C1, _ c2: C2
) -> Bool where C1.Item == C2.Item, C1.Item: Equatable {
guard c1.count == c2.count else { return false }
for i in 0..<c1.count {
if c1[i] != c2[i] { return false }
}
return true
}
var box1 = Box<Int>()
box1.append(1); box1.append(2); box1.append(3)
var box2 = Box<Int>()
box2.append(1); box2.append(2); box2.append(3)
print("All match: \(allItemsMatch(box1, box2))")
Output:
TEXT 📖 Display onlyAll match: true
6. Full Example: Generic Queue and Cache System
// ============================================
// Full example: Generic queue + bounded cache
// Features: general-purpose queue, cache wrapper, statistics
// ============================================
import Foundation
// 1. Generic queue
struct Queue<Element> {
private var items: [Element] = []
mutating func enqueue(_ item: Element) { items.append(item) }
mutating func dequeue() -> Element? { items.isEmpty ? nil : items.removeFirst() }
var count: Int { items.count }
}
// 2. Generic cache (with capacity limit)
class Cache<Key: Hashable, Value> {
private var storage: [Key: Value] = [:]
private let capacity: Int
init(capacity: Int = 100) {
self.capacity = capacity
}
func set(_ value: Value, for key: Key) {
if storage.count >= capacity {
storage.removeFirst()
}
storage[key] = value
}
func get(for key: Key) -> Value? { storage[key] }
var count: Int { storage.count }
}
// 3. Usage example
var queue = Queue<String>()
queue.enqueue("Task 1")
queue.enqueue("Task 2")
queue.enqueue("Task 3")
print("Queue count: \(queue.count)")
print("Dequeue: \(queue.dequeue() ?? "")")
let cache = Cache<String, Int>(capacity: 3)
cache.set(42, for: "answer")
cache.set(100, for: "score")
print("Cached answer: \(cache.get(for: "answer") ?? 0)")
print("Cache count: \(cache.count)")
Output:
TEXT 📖 Display onlyQueue count: 3 Dequeue: Task 1 Cached answer: 42 Cache count: 2
❓ FAQ
Array<Element>, where Element is the generic parameter. All array operations (append, sort, map) are generic methods.📖 Summary
- Generic functions use
<T>to declare type parameters; concrete types are inferred at the call site - Generic types (structs/classes/enums) can handle data of any type with a single implementation
- Type constraints
<T: Protocol>limit the scope of generic parameters and ensure type safety - Associated types
associatedtypelet protocols support generics; the implementer decides the concrete type - where clauses express complex multi-condition type relationships
- The standard library heavily uses generics: Array, Dictionary, and Optional are all generic types
📝 Exercises
- Basic: Write a generic function
findMax<T: Comparable>that accepts two parameters and returns the larger one. Test with Int, Double, and String types. - Intermediate: Extend the
Stacktype from Section 4 by adding afiltermethod that accepts a(Element) -> Boolclosure and returns a filtered new stack. - Challenge: Use generics to implement a
RingBuffer<Element>circular buffer withwrite(_:)andread() -> Element?methods, a fixed capacity, and automatic overwriting of the oldest data when full.