Why Generics?
Imagine writing a function that swaps two values. Without generics, you'd need separate versions for Int, String, Double, and every other type. Generics let you write it once and have it work with any type โ while keeping full type safety.
func swapTwo(_ a: inout T, _ b: inout T) {
let temp = a
a = b
b = temp
}
var x = 10
var y = 20
swapTwo(&x, &y)
print(x, y) // 20 10
var hello = "hello"
var world = "world"
swapTwo(&hello, &world)
print(hello, world) // world hello
Try it Yourself ->
Generic Functions
The angle brackets <T> define a type parameter. T is a placeholder โ Swift figures out the actual type when you call the function. You can use multiple type parameters if needed.
func first(_ array: [T]) -> T? {
return array.first
}
let numbers = [1, 2, 3]
let names = ["Alice", "Bob", "Charlie"]
print(first(numbers)) // Optional(1)
print(first(names)) // Optional("Alice")
Try it Yourself ->
Generic Types
Generics aren't just for functions โ you can build entire types that work with any element. A stack is a classic example. Push, pop, peek โ all work whether you're storing integers, strings, or your own custom objects.
struct Stack<Element> {
private var items: [Element] = []
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element? {
return items.popLast()
}
func peek() -> Element? {
return items.last
}
var isEmpty: Bool {
return items.isEmpty
}
}
var intStack = Stack<Int>()
intStack.push(1)
intStack.push(2)
print(intStack.pop()) // Optional(2)
var stringStack = Stack<String>()
stringStack.push("hello")
print(stringStack.peek()) // Optional("hello")
Try it Yourself ->
Type Constraints
Sometimes you need to restrict which types can be used. Add constraints with a colon โ the type must conform to a protocol or inherit from a class. This keeps your generic code flexible but safe.
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]
print(findIndex(of: 20, in: numbers)) // Optional(1)
let words = ["apple", "banana", "cherry"]
print(findIndex(of: "banana", in: words)) // Optional(1)
Try it Yourself ->
Associated Types in Protocols
Protocols can have associated types โ placeholders that get filled in when a type adopts the protocol. This lets you write protocols that work with generic types without losing flexibility.
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
struct IntStack: Container {
typealias Item = Int
private var items: [Int] = []
mutating func append(_ item: Int) {
items.append(item)
}
var count: Int { items.count }
subscript(i: Int) -> Int { items[i] }
}
Try it Yourself ->
Generic Where Clauses
You can add conditions to generics using a where clause. This is useful when you need specific behavior from the types you're working with.
func allMatch<T: Equatable>(_ array: [T], _ value: T) -> Bool {
return array.allSatisfy { $0 == value }
}
print(allMatch([1, 1, 1], 1)) // true
print(allMatch([1, 2, 1], 1)) // false
Try it Yourself ->