Generic Functions & Types
Generic Functions & Types
Generics allow you to write code that works with any type while maintaining type safety. They eliminate code duplication by abstracting over types.
Generic Functions
Without generics, you might write separate functions for each type:
func 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
}
With generics, one function handles all types:
func swapValues<T>(_ a: inout T, _ b: inout T) {
let temp = a
a = b
b = temp
}
var x = 10, y = 20
swapValues(&x, &y) // x=20, y=10
var a = "hello", b = "world"
swapValues(&a, &b) // a="world", b="hello"
T is a type parameter—a placeholder for any type. The compiler infers the concrete type at the call site.
Generic Types
You can make structs, classes, and enums generic:
struct Stack<Element> {
private var items: [Element] = []
mutating func push(_ element: Element) {
items.append(element)
}
mutating func pop() -> Element? {
return items.popLast()
}
var count: Int { items.count }
var isEmpty: Bool { items.isEmpty }
}
var intStack = Stack<Int>()
intStack.push(1)
intStack.push(2)
intStack.pop() // Optional(2)
var stringStack = Stack<String>()
stringStack.push("hello")
Multiple Type Parameters
func merge<T, U>(_ a: T, _ b: U) -> String {
return "\(a) + \(b)"
}
merge(1, "hello") // "1 + hello"
Generic Enums
enum Result<Success, Failure> {
case success(Success)
case failure(Failure)
}
let result: Result<Int, String> = .success(42)
Generic Closures
func apply<T>(_ value: T, transform: (T) -> T) -> T {
return transform(value)
}
let doubled = apply(5) { $0 * 2 } // 10
Type Inference with Generics
The compiler usually infers generic types automatically:
func first<T>(_ array: [T]) -> T? {
return array.first
}
let nums = [1, 2, 3]
let result = first(nums) // type inferred as Int?
Generics are the foundation of Swift's type-safe collections and standard library. Mastering them unlocks powerful abstractions.
Type Constraints
Type Constraints
Type constraints restrict which types can be used as generic parameters, ensuring they have specific capabilities.
Basic Constraints
func findIndex<T: Equatable>(of value: T, in array: [T]) -> Int? {
for (index, item) in array.enumerated() {
if item == value { // requires Equatable
return index
}
}
return nil
}
findIndex(of: 3, in: [1, 2, 3]) // Optional(2)
findIndex(of: "b", in: ["a", "b"]) // Optional(1)
// findIndex(of: [1], in: [[1]]) // Compile error — [Int] not Equatable
T: Equatable means T must conform to Equatable.
Multiple Constraints
func process<T: Codable & Hashable>(_ value: T) -> String {
return "Processed: \(value)"
}
Where Clauses
For complex constraints, use a where clause:
func merge<C1: Container, C2: Container>(
_ c1: C1, _ c2: C2
) where C1.Item == C2.Item, C1.Item: Equatable {
// Both containers have the same Equatable item type
}
Constraining to Protocols
protocol Printable {
func formatted() -> String
}
func printAll<T: Printable>(_ items: [T]) {
items.forEach { print($0.formatted()) }
}
Constraining to Classes
Use AnyObject or class to restrict to reference types:
func referenceOnly<T: AnyObject>(_ value: T) {
// Only classes can be passed
}
Default Generic Constraints
You can provide default type arguments:
struct Repository<T: Identifiable> {
var items: [T] = []
mutating func add(_ item: T) {
items.append(item)
}
func find(byId id: T.ID) -> T? {
items.first { $0.id == id }
}
}
Opaque Types with Constraints
func makeCollection() -> some Collection where Element == Int {
return [1, 2, 3]
}
Practical Example
protocol Cacheable: Codable {
var cacheKey: String { get }
}
class Cache<T: Cacheable> {
private var storage: [String: T] = [:]
func store(_ item: T) {
storage[item.cacheKey] = item
}
func retrieve(forKey key: String) -> T? {
storage[key]
}
}
struct User: Cacheable {
let cacheKey: String
let name: String
}
let userCache = Cache<User>()
userCache.store(User(cacheKey: "u1", name: "Alice"))
userCache.retrieve(forKey: "u1") // Optional(User)
Type constraints ensure generic code is both flexible and correct, preventing runtime errors at compile time.
Associated Types
Associated Types
Associated types let protocols define placeholder types that conforming types fill in. They are the protocol equivalent of generic type parameters.
Basic Associated Type
protocol Container {
associatedtype Item
var count: Int { get }
mutating func push(_ item: Item)
mutating func pop() -> Item?
}
struct IntStack: Container {
typealias Item = Int // optional — compiler infers this
private var items: [Int] = []
var count: Int { items.count }
mutating func push(_ item: Int) { items.append(item) }
mutating func pop() -> Int? { items.popLast() }
}
struct StringStack: Container {
// Item is inferred as String from push/pop signatures
private var items: [String] = []
var count: Int { items.count }
mutating func push(_ item: String) { items.append(item) }
mutating func pop() -> String? { items.popLast() }
}
Where Clauses with Associated Types
extension Container where Item: Equatable {
func contains(_ item: Item) -> Bool {
// simplified
return false
}
}
extension Container where Item == Int {
func sum() -> Int {
// simplified
return 0
}
}
Associated Type Constraints
You can constrain associated types to conform to protocols:
protocol IterableContainer {
associatedtype Item: Equatable
var items: [Item] { get }
}
Associated Type Inference
The compiler infers associated types from method signatures:
struct NumberCollection: IterableContainer {
var items: [Int] // Item inferred as Int
}
When to Use Associated Types vs Generics
| Scenario | Use |
|---|---|
| Protocol defining a type relationship | associatedtype |
| Concrete type that can vary | Generic <T> |
| Protocol with flexible return types | associatedtype |
Practical Example
protocol Repository {
associatedtype Entity: Identifiable
func findAll() -> [Entity]
func findById(_ id: Entity.ID) -> Entity?
func save(_ entity: Entity)
}
struct UserRepository: Repository {
typealias Entity = User
private var users: [User] = []
func findAll() -> [User] { users }
func findById(_ id: String) -> User? {
users.first { $0.id == id }
}
func save(_ user: User) {
users.append(user)
}
}
struct User: Identifiable {
let id: String
var name: String
}
Associated types enable powerful abstractions in protocol-oriented design. They let protocols describe relationships between types without committing to specific concrete types.
Quiz
1. What does `<T>` represent in a generic function?
2. What does `func foo<T: Equatable>(...)` constrain?
3. What is an associated type in a protocol?
4. When should you use `associatedtype` vs generic `<T>`?
5. What does `where T: Codable & Hashable` do?
Flashcards
Question
What is a generic function?
Click to reveal answer
Answer
A function with a type parameter `<T>` that works with any type while maintaining type safety.
Question
What is a type constraint?
Click to reveal answer
Answer
A restriction on generic parameters, e.g., `<T: Equatable>`, ensuring the type has specific capabilities.
Question
What is an associated type?
Click to reveal answer
Answer
A placeholder type in a protocol that conforming types fill in, like `associatedtype Item`.
Question
How do you constrain an associated type?
Click to reveal answer
Answer
Use `associatedtype Item: Protocol` to require the associated type to conform to a protocol.
Question
What does `typealias Item = Int` do in a conforming type?
Click to reveal answer
Answer
Explicitly satisfies an associated type requirement by mapping it to a concrete type.
Revision Notes
Key Takeaways
- 1. Generics eliminate code duplication while preserving type safety
- 2. Type constraints ensure generic types have required capabilities
- 3. Associated types define placeholder types in protocols
- 4. Where clauses provide complex generic constraints
- 5. The compiler infers generic types at the call site
Interview Tips
- • Explain when to use generics vs associated types
- • Know how to write type-constrained generic functions
- • Be able to design a generic Stack or Queue
- • Understand associated type constraints with where clauses
Cheat Sheet
Generics Cheat Sheet
Generic Function:
func swap<T>(_ a: inout T, _ b: inout T) { ... }
Generic Type:
struct Stack<Element> { ... }
Type Constraint:
func foo<T: Equatable>(...) { ... }
func bar<T: Codable & Hashable>(...) { ... }
Where Clause:
func baz<T>() where T: Comparable, T: Numeric { ... }
Associated Type:
protocol Container {
associatedtype Item
func push(_ item: Item)
}
Constraining Associated Type:
extension Container where Item: Equatable { ... }