Hey there, Swifties! Whether you’re just starting out or looking to level up your Swift game, this post will walk you through everything you need to know about generics, complete with plenty of examples.
What Are Generics?
At its core, generics allow you to write flexible, reusable functions and types that can work with any type, subject to the requirements you define. They’re a way to tell the Swift compiler that a function or type can work with any type.
Let’s start with a simple example:
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
let temporaryA = a
a = b
b = temporaryA
}
This swapTwoValues(_:_:)
function can swap two values of any type. The placeholder type T
is an example of a type parameter. When you call the function, Swift replaces T
with an actual type based on the values you pass in.
Generic Functions
Let’s look at how we might use our swapTwoValues(_:_:)
function:
var someInt = 3
var anotherInt = 107
swapTwoValues(&someInt, &anotherInt)
print("someInt is now \(someInt), and anotherInt is now \(anotherInt)")
// Prints "someInt is now 107, and anotherInt is now 3"
var someString = "hello"
var anotherString = "world"
swapTwoValues(&someString, &anotherString)
print("someString is now \(someString), and anotherString is now \(anotherString)")
// Prints "someString is now world, and anotherString is now hello"
As you can see, we can use the same function to swap integers and strings. Pretty cool, right?
Generic Types
Now, let’s step it up a notch and look at generic types. Here’s a generic stack implementation:
struct Stack<Element> {
var items: [Element] = []
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
}
We can create stacks of any type:
var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
let fromTheTop = stackOfStrings.pop()
print(fromTheTop) // Prints "tres"
Type Constraints
Sometimes, we want to use generics but need to make sure the types we’re working with have certain capabilities. That’s where type constraints come in. Let’s look at an example:
func findIndex<T: Equatable>(of valueToFind: T, in array:[T]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}
Here, we’re constraining T
to types that conform to the Equatable
protocol. This ensures we can use the ==
operator to compare values.
Associated Types
Associated types are a powerful feature that allow protocols to be generic. Let’s look at the Container
protocol:
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
We can implement this protocol with our Stack
type:
extension Stack: Container {
mutating func append(_ item: Element) {
self.push(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> Element {
return items[i]
}
}
Generic Where Clauses
Generic where clauses allow you to define requirements for associated types. Here’s an example:
func allItemsMatch<C1: Container, C2: Container>
(_ someContainer: C1, _ anotherContainer: C2) -> Bool
where C1.Item == C2.Item, C1.Item: Equatable {
// Check that both containers contain the same number of items
if someContainer.count != anotherContainer.count {
return false
}
// Check each pair of items to see if they're equivalent
for i in 0..<someContainer.count {
if someContainer[i] != anotherContainer[i] {
return false
}
}
// All items match, so return true
return true
}
//Usage
var stackOfStrings1 = Stack<String>()
stackOfStrings.push("uno")
var stackOfStrings2 = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
print(allItemsMatch(stackOfStrings1, stackOfStrings2))
// Prints false
This function checks if two containers have the same items in the same order. The where clause specifies that the Item types of both containers must be the same and must conform to Equatable
.
Opaque Types
Introduced in Swift 5.1, opaque types allow you to hide the concrete return type of a function or method. They’re defined using the some
keyword:
protocol Shape {
func draw() -> String
}
struct Triangle: Shape {
func draw() -> String {
return "▲"
}
}
struct Square: Shape {
func draw() -> String {
return "■"
}
}
func makeShape() -> Shape {
let someCondition = Bool.random()
return someCondition ? Triangle() : Square()
}
The makeShape()
function returns “Shape” – we know it returns a Shape
, but we don’t know (and don’t need to know) which specific type of Shape
it is.