Skip to content

Generics

Generics enable reusable, type-safe code by abstracting over types, reducing duplication while maintaining compile-time safety. They are integral to Swift’s standard library (e.g., Array, Dictionary) and support flexible function, type, and protocol definitions.

Generic Functions

Define functions with type parameters using <T> to work with any type.

Example: Basic Generic Function:

swift
func swap<T>(_ a: inout T, _ b: inout T) {
    let temp = a
    a = b
    b = temp
}

var x = 5, y = 10
swap(&x, &y)
print(x, y) // 10, 5

var s1 = "Apple", s2 = "Banana"
swap(&s1, &s2)
print(s1, s2) // "Banana", "Apple"

Example: Generic Processing:

swift
func process<T>(_ items: [T], transform: (T) -> T) -> [T] {
    return items.map(transform)
}

let numbers = process([1, 2, 3]) { $0 * 2 } // [2, 4, 6]
let strings = process(["a", "b"]) { $0.uppercased() } // ["A", "B"]

Generic Types

Create structs, classes, or enums with generic parameters for reusable data structures.

Example: Generic Stack:

swift
struct Stack<T> {
    private var elements: [T] = []
    
    mutating func push(_ item: T) {
        elements.append(item)
    }
    
    mutating func pop() -> T? {
        return elements.popLast()
    }
    
    var peek: T? {
        return elements.last
    }
}

var intStack = Stack<Int>()
intStack.push(1)
intStack.push(2)
print(intStack.pop()) // 2
print(intStack.peek) // 1

var stringStack = Stack<String>()
stringStack.push("Hello")
print(stringStack.peek) // "Hello"

Example: Generic Tree:

swift
class TreeNode<T> {
    var value: T
    var children: [TreeNode<T>] = []
    
    init(value: T) {
        self.value = value
    }
    
    func addChild(_ value: T) {
        children.append(TreeNode(value: value))
    }
}

let root = TreeNode(value: "Root")
root.addChild("Child1")
root.addChild("Child2")
print(root.children.map { $0.value }) // ["Child1", "Child2"]

Type Constraints

Restrict generic types using protocols or superclasses.

Example: Comparable Constraint:

swift
func findMax<T: Comparable>(_ array: [T]) -> T? {
    return array.max()
}

print(findMax([3, 1, 4, 1, 5])) // 5
print(findMax(["apple", "zebra", "banana"])) // "zebra"

Example: Protocol Constraint:

swift
protocol Identifiable {
    associatedtype ID
    var id: ID { get }
}

func findByID<T: Identifiable>(_ items: [T], id: T.ID) -> T? where T.ID: Equatable {
    return items.first { $0.id == id }
}

struct User: Identifiable {
    let id: String
    let name: String
}

let users = [User(id: "u1", name: "Alice"), User(id: "u2", name: "Bob")]
print(findByID(users, id: "u1")?.name) // "Alice"

Associated Types in Protocols

Use associatedtype for generic protocols, allowing conforming types to specify the type.

Example:

swift
protocol Container {
    associatedtype Item
    var items: [Item] { get }
    mutating func add(_ item: Item)
}

struct Queue<T>: Container {
    var items: [T] = []
    mutating func add(_ item: T) {
        items.append(item)
    }
}

var queue = Queue<Int>()
queue.add(1)
queue.add(2)
print(queue.items) // [1, 2]

Where Clauses

Add constraints to generic types, associated types, or parameters.

Example:

swift
func merge<C1: Container, C2: Container>(_ c1: C1, _ c2: C2) -> [C1.Item] where C1.Item == C2.Item {
    return c1.items + c2.items
}

struct ArrayContainer<T>: Container {
    var items: [T]
}

let c1 = ArrayContainer(items: [1, 2])
let c2 = Queue(items: [3, 4])
print(merge(c1, c2)) // [1, 2, 3, 4]

Opaque Types

Return a specific type conforming to a protocol without exposing it, using some.

Example:

swift
protocol Shape {
    func draw()
}

struct Circle: Shape {
    func draw() { print("Drawing Circle") }
}

func makeShape(_ type: String) -> some Shape {
    return Circle()
}

let shape = makeShape("circle")
shape.draw() // "Drawing Circle"

Generic Subscripts

Define subscripts with generic return types.

Example:

swift
struct DataStore<K: Hashable, V> {
    private var storage: [K: V]
    
    subscript<T>(key: K, as type: T.Type) -> T? {
        return storage[key] as? T
    }
}

var store = DataStore<String, Any>(storage: ["score": 95, "name": "Alice"])
print(store["score", as: Int.self]) // 95
print(store["name", as: String.self]) // "Alice"

Conditional Conformance

Make types conform to protocols conditionally.

Example:

swift
extension Array: Identifiable where Element: Identifiable {
    var id: String {
        return elements.map { "\($0.id)" }.joined(separator: "-")
    }
}

let identifiableUsers = [User(id: "u1", name: "Alice"), User(id: "u2", name: "Bob")]
print(identifiableUsers.id) // "u1-u2"

Best Practices

  • Use Constraints: Ensure generics provide necessary functionality.
  • Opaque Types: Hide implementation details for cleaner APIs.
  • Where Clauses: Clarify complex type relationships.
  • Conditional Conformance: Leverage for flexible protocol adoption.
  • Avoid Over-Genericity: Balance with concrete types for readability.
  • Document Generics: Clearly explain type parameters and constraints.

Troubleshooting

  • Type Ambiguity: Add type annotations or constraints.
  • Constraint Violations: Verify protocol or superclass conformance.
  • Opaque Type Errors: Ensure consistent return types.
  • Associated Type Conflicts: Use typealias or explicit constraints.
  • Performance: Minimize generic complexity in performance-critical code.

Example: Comprehensive Generic Usage

swift
protocol DataStore {
    associatedtype Key: Hashable
    associatedtype Value
    subscript(key: Key) -> Value? { get set }
}

class Cache<K: Hashable, V>: DataStore {
    private var storage: [K: V] = [:]
    
    subscript(key: K) -> V? {
        get { storage[key] }
        set { storage[key] = newValue }
    }
    
    func evictAll() {
        storage.removeAll()
    }
}

func mergeStores<S1: DataStore, S2: DataStore>(_ s1: S1, _ s2: S2) -> [S1.Key: S1.Value] where S1.Key == S2.Key, S1.Value == S2.Value {
    var result = s1.storage
    for (key, value) in s2.storage {
        result[key] = value
    }
    return result
}

let cache1 = Cache<String, Int>()
cache1["a"] = 1
cache1["b"] = 2

let cache2 = Cache<String, Int>()
cache2["b"] = 3
cache2["c"] = 4

let merged = mergeStores(cache1, cache2)
print(merged) // ["a": 1, "b": 3, "c": 4]

Released under the MIT License.