Swift Generics - Writing Flexible, Reusable Code
Welcome to Swift Generics! Generics are one of Swift’s most powerful features, allowing you to write flexible, reusable functions and types that can work with any type. In this guide, we’ll explore how to use generics to write clean, type-safe, and efficient code.
What Are Generics?
Generics enable you to write code that works with any type, while still maintaining type safety. Instead of writing separate functions for each type, you write one generic function.
Why Use Generics?
- ✅ Code Reusability - Write once, use with many types
- ✅ Type Safety - Compile-time type checking
- ✅ Flexibility - Work with any type
- ✅ Performance - No runtime overhead
- ✅ Clean Code - Avoid duplication
Example Problem Without Generics:
// ❌ Duplicated code for each typefunc swapInts(_ a: inout Int, _ b: inout Int) { let temp = a a = b b = temp}
func swapStrings(_ a: inout String, _ b: inout String) { let temp = a a = b b = temp}
// Need to write this for every type!Generic Functions
Basic Generic Function
func swap<T>(_ a: inout T, _ b: inout T) { let temp = a a = b b = temp}
var x = 5var y = 10swap(&x, &y)print("x: \(x), y: \(y)") // x: 10, y: 5
var name1 = "Alice"var name2 = "Bob"swap(&name1, &name2)print("name1: \(name1), name2: \(name2)") // name1: Bob, name2: AliceGeneric Function with Return Value
func makeArray<T>(repeating item: T, count: Int) -> [T] { var result: [T] = [] for _ in 0..<count { result.append(item) } return result}
let integers = makeArray(repeating: 5, count: 3)print(integers) // [5, 5, 5]
let strings = makeArray(repeating: "Hello", count: 2)print(strings) // ["Hello", "Hello"]Multiple Type Parameters
func printPair<T, U>(_ first: T, _ second: U) { print("First: \(first), Second: \(second)")}
printPair(42, "Answer") // First: 42, Second: AnswerprintPair(3.14, true) // First: 3.14, Second: trueGeneric Function with Dictionary
func makeDictionary<Key: Hashable, Value>(keys: [Key], values: [Value]) -> [Key: Value] { var dict: [Key: Value] = [:] for (index, key) in keys.enumerated() { if index < values.count { dict[key] = values[index] } } return dict}
let dict = makeDictionary(keys: ["a", "b", "c"], values: [1, 2, 3])print(dict) // ["a": 1, "b": 2, "c": 3]Generic Types
Generic Stack
struct Stack<Element> { private var items: [Element] = []
var isEmpty: Bool { return items.isEmpty }
var count: Int { return items.count }
mutating func push(_ item: Element) { items.append(item) }
mutating func pop() -> Element? { return items.popLast() }
func peek() -> Element? { return items.last }}
var intStack = Stack<Int>()intStack.push(1)intStack.push(2)intStack.push(3)print(intStack.pop() ?? 0) // 3
var stringStack = Stack<String>()stringStack.push("Hello")stringStack.push("World")print(stringStack.peek() ?? "") // WorldGeneric Pair
struct Pair<T, U> { var first: T var second: U
init(_ first: T, _ second: U) { self.first = first self.second = second }
func swapped() -> Pair<U, T> { return Pair<U, T>(second, first) }}
let pair = Pair(42, "Answer")print("\(pair.first): \(pair.second)") // 42: Answer
let swapped = pair.swapped()print("\(swapped.first): \(swapped.second)") // Answer: 42Generic Wrapper
struct Box<T> { var value: T
func map<U>(_ transform: (T) -> U) -> Box<U> { return Box<U>(value: transform(value)) }}
let intBox = Box(value: 42)let stringBox = intBox.map { String($0) }print(stringBox.value) // "42"
let doubled = intBox.map { $0 * 2 }print(doubled.value) // 84Type Constraints
Restrict generic types to types that conform to specific protocols or inherit from specific classes.
Basic Type Constraint
func findIndex<T: Equatable>(of item: T, in array: [T]) -> Int? { for (index, value) in array.enumerated() { if value == item { return index } } return nil}
let numbers = [1, 2, 3, 4, 5]if let index = findIndex(of: 3, in: numbers) { print("Found at index \(index)") // Found at index 2}
let names = ["Alice", "Bob", "Charlie"]if let index = findIndex(of: "Bob", in: names) { print("Found at index \(index)") // Found at index 1}Multiple Constraints
func compare<T: Comparable & CustomStringConvertible>(_ a: T, _ b: T) { if a < b { print("\(a) is less than \(b)") } else if a > b { print("\(a) is greater than \(b)") } else { print("\(a) equals \(b)") }}
compare(5, 10) // 5 is less than 10compare("apple", "banana") // apple is less than bananaConstrained Generic Type
struct SortedArray<Element: Comparable> { private var items: [Element] = []
var count: Int { return items.count }
mutating func insert(_ item: Element) { if let index = items.firstIndex(where: { $0 > item }) { items.insert(item, at: index) } else { items.append(item) } }
func get(at index: Int) -> Element? { return items.indices.contains(index) ? items[index] : nil }
var all: [Element] { return items }}
var sortedNumbers = SortedArray<Int>()sortedNumbers.insert(5)sortedNumbers.insert(2)sortedNumbers.insert(8)sortedNumbers.insert(1)print(sortedNumbers.all) // [1, 2, 5, 8]Associated Types in Protocols
Protocols can have associated types as placeholders for types.
Container Protocol
protocol Container { associatedtype Item var count: Int { get } mutating func append(_ item: Item) subscript(i: Int) -> Item { get }}
struct IntStack: Container { // typealias Item = Int (inferred) private var items: [Int] = []
var count: Int { return items.count }
mutating func append(_ item: Int) { items.append(item) }
subscript(i: Int) -> Int { return items[i] }}
var stack = IntStack()stack.append(10)stack.append(20)print(stack[0]) // 10print(stack.count) // 2Generic Container
struct GenericStack<Element>: Container { private var items: [Element] = []
var count: Int { return items.count }
mutating func append(_ item: Element) { items.append(item) }
subscript(i: Int) -> Element { return items[i] }}
var stringStack = GenericStack<String>()stringStack.append("Hello")stringStack.append("World")print(stringStack[1]) // WorldWhere Clauses
protocol Container { associatedtype Item var count: Int { get } subscript(i: Int) -> Item { get }}
func allItemsMatch<C1: Container, C2: Container>(_ container1: C1, _ container2: C2) -> Bool where C1.Item == C2.Item, C1.Item: Equatable {
guard container1.count == container2.count else { return false }
for i in 0..<container1.count { if container1[i] != container2[i] { return false } }
return true}Practical Examples
Example 1: Generic Result Type
enum Result<Success, Failure: Error> { case success(Success) case failure(Failure)
func map<NewSuccess>(_ transform: (Success) -> NewSuccess) -> Result<NewSuccess, Failure> { switch self { case .success(let value): return .success(transform(value)) case .failure(let error): return .failure(error) } }}
enum NetworkError: Error { case noConnection case timeout}
func fetchData() -> Result<String, NetworkError> { return .success("User data")}
let result = fetchData()let transformed = result.map { $0.uppercased() }
switch transformed {case .success(let data): print(data) // USER DATAcase .failure(let error): print("Error: \(error)")}Example 2: Generic Queue
struct Queue<Element> { private var items: [Element] = []
var isEmpty: Bool { return items.isEmpty }
var count: Int { return items.count }
var front: Element? { return items.first }
mutating func enqueue(_ item: Element) { items.append(item) }
mutating func dequeue() -> Element? { return isEmpty ? nil : items.removeFirst() }}
var queue = Queue<String>()queue.enqueue("First")queue.enqueue("Second")queue.enqueue("Third")
while let item = queue.dequeue() { print(item)}// Output:// First// Second// ThirdExample 3: Generic Cache
class Cache<Key: Hashable, Value> { private var storage: [Key: Value] = [:]
func get(_ key: Key) -> Value? { return storage[key] }
func set(_ key: Key, value: Value) { storage[key] = value }
func remove(_ key: Key) { storage.removeValue(forKey: key) }
func clear() { storage.removeAll() }
var count: Int { return storage.count }}
let userCache = Cache<String, String>()userCache.set("user1", value: "Alice")userCache.set("user2", value: "Bob")
if let user = userCache.get("user1") { print("Found: \(user)") // Found: Alice}
print("Cache size: \(userCache.count)") // Cache size: 2Example 4: Generic Tree
class TreeNode<T> { var value: T var children: [TreeNode<T>] = []
init(_ value: T) { self.value = value }
func addChild(_ child: TreeNode<T>) { children.append(child) }
func traverse(visit: (T) -> Void) { visit(value) children.forEach { $0.traverse(visit: visit) } }}
let root = TreeNode(1)let child1 = TreeNode(2)let child2 = TreeNode(3)let grandchild = TreeNode(4)
root.addChild(child1)root.addChild(child2)child1.addChild(grandchild)
print("Tree traversal:")root.traverse { print($0) }// Output:// 1// 2// 4// 3Example 5: Generic Repository Pattern
protocol Entity { var id: String { get }}
class Repository<T: Entity> { private var items: [String: T] = [:]
func save(_ item: T) { items[item.id] = item }
func find(by id: String) -> T? { return items[id] }
func findAll() -> [T] { return Array(items.values) }
func delete(by id: String) { items.removeValue(forKey: id) }
var count: Int { return items.count }}
struct User: Entity { let id: String var name: String var email: String}
struct Product: Entity { let id: String var name: String var price: Double}
let userRepo = Repository<User>()userRepo.save(User(id: "1", name: "Alice", email: "alice@email.com"))userRepo.save(User(id: "2", name: "Bob", email: "bob@email.com"))
if let user = userRepo.find(by: "1") { print("Found user: \(user.name)") // Found user: Alice}
let productRepo = Repository<Product>()productRepo.save(Product(id: "P1", name: "iPhone", price: 999.99))print("Products: \(productRepo.count)") // Products: 1Example 6: Generic Optional Extensions
extension Optional { func unwrap(or defaultValue: Wrapped) -> Wrapped { return self ?? defaultValue }
func map<U>(_ transform: (Wrapped) throws -> U) rethrows -> U? { guard let value = self else { return nil } return try transform(value) }}
let optional: Int? = nilprint(optional.unwrap(or: 42)) // 42
let value: Int? = 10let doubled = value.map { $0 * 2 }print(doubled ?? 0) // 20Generic Extensions
Extend generic types with additional functionality:
extension Stack where Element: Equatable { func contains(_ item: Element) -> Bool { return items.contains(item) }}
extension Stack where Element: Comparable { func max() -> Element? { return items.max() }
func min() -> Element? { return items.min() }}
var numberStack = Stack<Int>()numberStack.push(5)numberStack.push(2)numberStack.push(8)
print(numberStack.contains(5)) // trueprint(numberStack.max() ?? 0) // 8Best Practices
1. Use Meaningful Type Parameter Names
// ✅ Good - descriptive namesstruct Dictionary<Key: Hashable, Value> { }struct Result<Success, Failure: Error> { }
// ❌ Less clearstruct Dictionary<A: Hashable, B> { }struct Result<T, E: Error> { }2. Apply Constraints When Needed
// ✅ Good - constrained to Comparablefunc findMax<T: Comparable>(_ array: [T]) -> T? { return array.max()}
// ❌ Too generic - won't work// func findMax<T>(_ array: [T]) -> T? {// return array.max() // Error: T doesn't conform to Comparable// }3. Use Where Clauses for Complex Constraints
// ✅ Good - readablefunc process<T>(_ items: [T]) where T: Codable & Equatable { // Process items}
// ❌ Less readablefunc process<T: Codable & Equatable>(_ items: [T]) { // Process items}4. Prefer Protocols Over Concrete Types
// ✅ Good - flexiblefunc save<T: Encodable>(_ item: T) { // Can save any Encodable type}
// ❌ Too specificfunc save(_ item: User) { // Only works with User}Summary
Generics enable powerful, flexible code:
Generic Functions 🔧
- Work with any type
- Type-safe at compile time
- Eliminate code duplication
Generic Types 📦
- Stack, Queue, Cache, etc.
- Reusable data structures
- Type parameters
Type Constraints 🔒
- Restrict to specific protocols
- Multiple constraints with &
- Where clauses for clarity
Associated Types 🎯
- Generic protocols
- Placeholder types
- Flexible protocol design
Benefits ⭐
- Code reuse
- Type safety
- Performance
- Flexibility
Next Steps
Congratulations! 🎉 You’ve completed a comprehensive journey through Swift’s core features!
What’s next:
- Practice building generic data structures
- Explore Swift Standard Library generics
- Build real-world projects using these concepts
- Dive into advanced Swift topics (async/await, actors, etc.)
Practice Exercises
- Create a generic
LinkedList<T>data structure - Build a generic
priorityQueue<T: Comparable> - Implement a generic
Either<Left, Right>type - Create a generic caching system with expiration
- Build a type-safe network client using generics
- Implement a generic validation framework
Master generics to write powerful, reusable Swift code! 🚀
Remember: Generics = flexibility + type safety!