Labs ICT
โญ Pro Login

Generics

Writing flexible, reusable code.

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

๐Ÿงช Quick Quiz

What keyword is used for generic type parameters?