Generics in Swift allows you to write generic and reusable code, avoiding duplication. A generic type or function creates constraints for the current scope, requiring input values to conform to these requirements.
You’ve probably been using generic implementations already since they’re all over the place in the Swift standard library. Though, using them in your implementation might still be scary since generics can look complex and hard to write. Writing generic code requires a certain mindset to generalize functions to make them reusable. I encourage you to read my articles covering opaque types and existentials as well, as you’ll likely use them alongside generics.
How to use generics in Swift
You can write generics in Swift to make existing code more reusable. A classic example is the one using a stack of elements, better known as an array in Swift:
struct IntStack {
var items: [Int] = []
mutating func push(_ item: Int) {
items.append(item)
}
mutating func pop() -> Int {
return items.removeLast()
}
}
In this example, we’ve created a stack of integers. We can push and pop elements, but they all have to conform to integers. A stack with support for integers only might be good at first for you to keep around in your project. However, we’ll start duplicating code as soon as we need a stack of strings:
struct StringStack {
var items: [String] = []
mutating func push(_ item: String) {
items.append(item)
}
mutating func pop() -> String {
return items.removeLast()
}
}
Both IntStack
and StringStack
have similar code implementations, resulting in code duplication and multiple types to maintain. Indicating duplicate code can be a great moment to consider rewriting your logic by making use of generics:
struct Stack<Element> {
var items: [Element] = []
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
}
We’ve now created a generic Stack
by defining a generic Element
. The array of items uses the generic element, resulting in the same type constraint for both push and pop methods.
Creating generic methods
The above stack examples make use of generics on the type level. We can use generics within methods as well, like in the following print example:
func printElement<T: CustomStringConvertible>(_ element: T) {
print(element)
}
Note that we introduced the concept of constraints in this example. An element passed into the print method must conform to the CustomStringConvertible
protocol, allowing us to convert the element to a string before printing.
You can also write the above method using a where clause:
func printElement<T>(_ element: T) where T: CustomStringConvertible {
print(element)
}
Deciding between using a where clause or not is a matter of taste since the outcome of generics and constraints will be the same.
Opaque types as a shorthand for generics
You can use opaque types instead of generics whenever you’re using generics in a single place. The above print method uses the generic T
only within the method definition, and you can therefore rewrite it as follows:
func printElement(_ element: some CustomStringConvertible) {
print(element.description)
}
You can read more about opaque types in my article Some keyword in Swift: Opaque types explained with code examples.
Using generic return types
You can also use generics as return types. For example, we could create a method to convert any element into an array:
func convertToArray<T>(_ element: T) -> [T] {
return [element]
}
let name = "Antoine"
let arrayOfNames = convertToArray(name)
While this example is useless since you can create such an array directly, it nicely demonstrates the concept of returning a generic type.
Creating protocol extensions using constraints
Protocol extensions allow you to set up constraints using the associated type. For example, we can create an extension for an array of strings as follows:
extension Array where Element == String {
func uppercaseAll() -> [Element] {
map { $0.uppercased() }
}
}
["Bernie", "Jaap", "Lady"].uppercaseAll() // Will be: ["BERNIE", "JAAP", "LADY"]
When should I use generics?
While generics allow you to write reusable code, it’s important to point out that it shouldn’t be your goal to write generics. You should always start with strongly typed classes and functions and only opt-in to generics if you know you need the flexibility provided by generic code.
Generics make your code more complex to maintain with the returned benefit of reusable code and less duplication. There is a tradeoff for you to make, which might be easier to accept based on your experience with generics. If you’re just getting started with Swift, I would like you to know that it’s okay if you start with writing less efficient, duplicated code.
My approach to determining whether generics are needed comes down to finding code used in multiple places and making that reusable in a single place. The outcome is a single piece of code to maintain and less duplication.
Moving code instead of using generics
Whenever you find yourself duplicating the same piece of code, it’s time to think about a way to make it reusable from multiple places. Doing so doesn’t always mean you’ll have to write generics as you might be able to move code first. Take the following example:
struct Person {
let name: String
}
var people = [
Person(name: "Antoine"),
Person(name: "Jordi"),
Person(name: "Bas")
]
func longestName() -> String {
return people
.sorted(by: { $0.name.count > $1.name.count })
.first!
.name
}
func shortestName() -> String {
return people
.sorted(by: { $0.name.count > $1.name.count })
.last!
.name
}
print(longestName()) // Prints: Antoine
print(shortestName()) // Prints: Bas
Both methods use the same sorting logic and only differ by selecting the first or last element. We could rewrite this using a generic method as follows:
extension Collection where Element == String {
func sortedByLength() -> [Element] {
sorted(by: { $0.count > $1.count })
}
}
func longestName() -> String {
return people
.map(\.name)
.sortedByLength()
.first!
}
func shortestName() -> String {
return people
.map(\.name)
.sortedByLength()
.last!
}
We now have the benefit of being able to reuse the sortedByLength
method on all arrays of strings. However, it might be easier at first to create a reusable method first:
var peopleSortedByNameLength: [Person] {
people.sorted(by: { $0.name.count > $1.name.count })
}
func longestName() -> String {
return peopleSortedByNameLength
.first!
.name
}
func shortestName() -> String {
return peopleSortedByNameLength
.last!
.name
}
In other words, consider whether you need the complexity of generics and only opt-in when you know you’ll benefit from the code reusability. If you feel comfortable enough to write generics, it will be a better solution since you’ll prepare your code for future cases in which you might need the reusable functionality. However, avoiding generics is okay if you’re not too familiar with them yet.
Conclusion
Generics allow you to prevent code duplicating by creating reusable code. Writing generic code should not be a goal on its own, and you should feel fine staying away from them if you’re uncomfortable writing them. Though, generics allow you to create sustainable code and prepare for future cases in which you need to reuse the same piece of code.
If you like to improve your Swift knowledge, check out the Swift category page. Feel free to contact me or tweet me on Twitter if you have any additional tips or feedback.
Thanks!