Generics
Swift enables us to create generic types, protocols, and functions, that aren’t tied to any specific concrete type — but can instead be used with any type that meets a given set of requirements.
Being a language that strongly emphasizes type safety, generics is an essential feature that’s core to many aspects of Swift — including its standard library, which uses generics quite heavily. Just look at some of its fundamental data structures, like Array
and Dictionary
, both of which are generics.
Generics enable the same type, protocol, or function, to be specialized for a large number of use cases. For example, since Array
is a generic, it allows us to create specialized instances of it for any kind of type — such as strings:
var array = ["One", "Two", "Three"]
array.append("Four")
// This won't compile, since the above array is specialized
// for strings, meaning that other values can't be inserted:
array.append(5)
// As we pull an element out of the array, we can still treat
// it like a normal string, since we have full type safety.
let characterCount = array[0].count
To create a generic of our own, we simply have to define what our generic types are, and optionally attach constraints to them. For example, here we’re creating a Container
type that can contain any value, along with a date:
struct Container<Value> {
var value: Value
var date: Date
}
Just like how we’re able to create specialized arrays and dictionaries, we can specialize the above Container
for any kind of value, such as strings or integers:
let stringContainer = Container(value: "Message", date: Date())
let intContainer = Container(value: 7, date: Date())
Note that we don’t need to specify what concrete types we’re specializing Container
with above — Swift’s type inference automatically figures out that stringContainer
is a Container<String>
instance, and that intContainer
is an instance of Container<Int>
.
Generics are especially useful when we’re writing code that could be applied to many different types. For example, we might use the above Container
to implement a generic Cache
, that can store any kind of value, for any kind of key. In this case, we also add a constraint to require Key
to conform to Hashable
, so that we can use it with a dictionary — like this:
class Cache<Key: Hashable, Value> {
private var values = [Key: Container<Value>]()
func insert(_ value: Value, forKey key: Key) {
let expirationDate = Date().addingTimeInterval(1000)
values[key] = Container(
value: value,
date: expirationDate
)
}
func value(forKey key: Key) -> Value? {
guard let container = values[key] else {
return nil
}
// If the container's date is in the past, then the
// value has expired, and we remove it from the cache.
guard container.date > Date() else {
values[key] = nil
return nil
}
return container.value
}
}
With the above in place, we’re now able to create type-safe caches for any of our types — for example users, or search results:
class UserManager {
private var cachedUsers = Cache<User.ID, User>()
...
}
class SearchController {
private var cachedResults = Cache<Query, [SearchResult]>()
...
}
Above we do need to specify what types that we’re specializing Cache
for, since there’s no way for the compiler to infer that information from the call site.
Individual functions can also be generic, regardless of where they are defined. For example, here we’re extending String
(which is not a generic type) to add a generic function that lets us easily append the IDs of all the elements within an array of Identifiable
values:
extension String {
mutating func appendIDs<T: Identifiable>(of values: [T]) {
for value in values {
append(" \(value.id)")
}
}
}
Even protocols can be generics! In fact, the above Identifiable
protocol is an example of just that, since it uses an associated type to enable it to be specialized with any kind of ID
type — like this:
protocol Identifiable {
associatedtype ID: Equatable & CustomStringConvertible
var id: ID { get }
}
What the above approach enables is for each individual type that conforms to Identifiable
to decide what kind of ID
that it wants to use — while still being able to take full advantage of all the generic code we’ve written for Identifiable
types (such as our String
extension above).
For example, here’s how an Article
type could use UUID
values as IDs, while a Tag
type might simply use integers:
struct Article: Identifiable {
let id: UUID
var title: String
var body: String
}
struct Tag: Identifiable {
let id: Int
var name: String
}
The above technique is really useful when we need certain data models to use specific kinds of IDs, for example to be compatible with another system, such as a server-side backend.
Again the compiler will do most of the heavy lifting for us above, since it’ll automatically infer that Article.ID
means UUID
, and Tag.ID
means Int
— based on the id
property of each individual type. Now both Article
and Tag
can be passed to any function that accepts a value conforming to Identifiable
, while still remaining distinct types, that even use their own separate kinds of identifiers.
That’s really the power of generics overall, that they enable us to write more easily reused code, while still enabling local specialization. Algorithms, data structures, and utilities are usually great candidates for generics — since they often just need the types that they work with to fulfill a certain set of requirements, rather than being tied to specific concrete types.
Thanks for reading! 🚀