Generics In Swift
Supercharging Your Code
Imagine you're writing a function or struct that works for any type, not just Int
or String
. That's where generics come into play. It's like giving your code superpowers to handle all sorts of types in a clean, reusable way, without writing the same logic over and over for different data types.
Swift’s generics let you write flexible, reusable code. You don’t need to duplicate logic for every type—you define the requirements, and Swift does the rest. It’s like setting the rules, but leaving the specifics open until they’re needed.
A quick reality check: you’ve already been using generics if you've ever used Swift's built-in types like Array
and Dictionary
. Both of these collections are generic, meaning they can hold any kind of data—whether it’s an array of Int
s, String
s, or even custom objects.
Solving Problems with Generics
Say you’ve got a simple function to swap two Int
values. Cool, but what if you want to swap String
s or Double
s? You'd have to rewrite the same function over and over. That’s where generics step in to save the day.
Here’s the non-generic function to swap two Int
s:
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let temp = a
a = b
b = temp
}
Simple enough, but now let’s generalize this bad boy into a generic function that can swap anything:
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
let temp = a
a = b
b = temp
}
See that <T>
? That’s a placeholder for “whatever type you throw at me.” The function doesn’t care if it’s Int
, String
, or Double
. As long as both values are the same type, it works like a charm.
Generics with Flexibility
The placeholder T
we used in swapTwoValues<T>
is called a type parameter. It’s like a flexible placeholder for any type, allowing your function or type to work with any data.
If you’ve worked with Array<Element>
or Dictionary<Key, Value>
, you’ve seen type parameters in action. The cool thing? You can use more than one if you need to. For example, if you’re making something that works with two types, you can use <T, U>
.
Building Reusable Structures
Generics don’t stop at functions—they can be used in structs, enums, and classes too! Imagine you’re building a stack (like a stack of plates). You can push a plate onto the stack or pop one off. But what if you want to stack different things? Enter generic types.
Here’s a stack that only works with Int
s:
struct IntStack {
var items: [Int] = []
mutating func push(_ item: Int) {
items.append(item)
}
mutating func pop() -> Int {
return items.removeLast()
}
}
That’s nice, but limiting. Let’s make it generic:
struct Stack<Element> {
var items: [Element] = []
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
}
Now, Stack<Element>
can hold anything—Int
s, String
s, whatever you need.
Add Some Flavor
Got a generic type like Stack
and want to add more functionality? You can use extensions without rewriting the whole thing. Let’s add a computed property to get the top item in the stack without popping it off:
extension Stack {
var topItem: Element? {
return items.isEmpty ? nil : items[items.count - 1]
}
}
Boom! Now you can peek at the top item, whether you’re working with a stack of Int
s or String
s.
Generics with Boundaries
Sometimes you don’t want your generic type to accept just anything. Maybe you need it to work only with types that can be compared, like with the ==
operator. For that, you use type constraints.
Here’s a generic function that finds the index of an item in an array. It only works for types that conform to Equatable
(meaning they can be compared with ==
):
func findIndex<T: Equatable>(of valueToFind: T, in array: [T]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}
By adding T: Equatable
, you’re saying, “Hey, I need T
to be something that can be compared for equality.”
Protocols Get in on the Action
Protocols can play with generics too, using associated types. These give protocols flexibility about the types they work with, without hard-coding them.
Here’s a protocol for a container that holds some items:
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
The associatedtype
lets the protocol define “some type” for the items it contains, but leaves the specifics open for any type that conforms to it. This means the container could hold Int
s, String
s, or anything else.
Wrapping It Up
Generics let you write cleaner, reusable code that adapts to different types without breaking a sweat. It’s all about flexibility—your code defines the rules, but the actual types stay flexible until runtime. Whether you're building flexible data structures or crafting functions that can handle any type, Swift generics have your back.
And remember, the next time you create an Array
, Dictionary
, or even write a custom type that needs to work with anything under the sun, generics are the powerhouse behind the scenes making it all possible.
That's the magic of Swift generics! They make your code super flexible, reusable, and able to handle all sorts of situations without breaking a sweat. Pretty slick, right?
Subscribe to my newsletter
Read articles from DAMIAN ROBINSON directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by