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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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
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]