Skip to Content
Course content

26: Generic Functions and Types

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Why can't I just use "Any" if I want a function to handle different types?

This is the first thing most people ask when they hit generics. I get it—Any seems like the shortcut. But here is the problem: Any throws away all your type information. If you put an Int into a list of Any, Swift forgets it was an Int. When you pull it back out, you have to manually cast it using as? Int, which is tedious and error-prone.

Generics are different. They aren't about "accepting anything"; they're about "placeholder types." When you use a generic, you're telling Swift: "I don't know what the type is yet, but whatever it is, it'll be the same type throughout this whole operation."

// The "Any" way (Clunky and unsafe)
func printFirst(items: [Any]) {
    if let first = items.first as? String {
        print("It's a string: \(first)")
    }
}

// The Generic way (Clean and type-safe)
func printFirst<T>(items: [T]) {
    if let first = items.first {
        print("The first item is \(first)") 
        // Swift knows 'first' is of type T, no casting needed!
    }
}

How do I actually build a generic type, like a custom container?

You've seen generic functions, but the real power comes when you apply this to structs or classes. Let's say you're building a Stack. Whether you're stacking integers, strings, or complex User objects, the logic for push and pop is identical. Why write three different versions?

By adding <T> (or any letter, though T for "Type" is the convention) after the struct name, you make the entire structure generic.

struct Stack<Element> {
    var items: [Element] = []
    
    mutating func push(_ item: Element) {
        items.append(item)
    }
    
    mutating func pop() -> Element? {
        return items.popLast()
    }
}

// Now I can make a stack for any type
var intStack = Stack<Int>()
intStack.push(10)

var nameStack = Stack<String>()
nameStack.push("Swift")

I personally prefer naming the placeholder Element or Value rather than T when I'm writing structs; it makes the code much more readable for whoever has to maintain it six months from now.

What if I need the generic type to actually be able to do something?

Here is where you'll hit a wall: if you use a generic T, Swift assumes it knows nothing about that type. You can't use +, >, or any specific methods because Swift can't guarantee that every possible type in the universe supports them. If you try to compare two T values, the compiler will yell at you.

To fix this, we use Type Constraints. You tell Swift, "T can be anything, as long as it conforms to this specific protocol." For example, if you want to find the largest item in an array, T must be Comparable.

func findMax<T: Comparable>(items: [T]) -> T? {
    var maxItem: T?
    
    for item in items {
        if maxItem == nil || item > maxItem! {
            maxItem = item
        }
    }
    return maxItem
}

let numbers = [1, 5, 3, 2]
print(findMax(items: numbers)!) // Works! Int is Comparable.

let strings = ["Apple", "Zebra", "Banana"]
print(findMax(items: strings)!) // Works! String is Comparable.

If you tried to pass an array of a custom User struct into this function, it would fail to compile—unless you made your User struct conform to Comparable. This is the "secret sauce" of Swift's type system: flexibility without sacrificing safety.




📋 Practical Task

Build a Generic DataCache

Your task is to create a generic caching system that can store a single value of any type, associated with a string key. This will simulate a simple settings or session cache.

  • Create a struct named DataCache that is generic over a type Value.
  • Inside the struct, create a dictionary called storage where the keys are String and the values are of type Value.
  • Implement a method save(key: String, value: Value) that adds the item to the dictionary.
  • Implement a method retrieve(key: String) -> Value? that returns the cached value.

To test your implementation, instantiate one DataCache for Int to store a "userAge" and another DataCache for String to store a "username". Ensure that you cannot accidentally save a string into the integer cache.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.