Why can’t certain protocols, like Equatable and Hashable, be referenced directly?
Basics article available: ProtocolsIf you’ve been doing Swift development for a while, chances are quite high that you’ve encountered a version of the following compiler error at some point:
Protocol 'Equatable' can only be used as a generic constraint because it has
Self or associated type requirements.
That’s something that happens when we try to reference a generic protocol directly — that is, a protocol that has either associated types, or requires the conforming type (which Self
refers to) to be known. For example, the built-in Equatable
protocol uses Self
within its declaration, like this:
protocol Equatable {
static func ==(lhs: Self, rhs: Self) -> Bool
}
That’s because an equality check can only be performed between two values that are known to be of the exact same type — so something like the following function wouldn’t work, since the two arguments passed to it could be of any type conforming to Equatable
:
func valueDidChange(from old: Equatable, to new: Equatable) {
guard old != new else {
return
}
...
}
The following, on the other hand, does work — since we’re now using Equatable
as a constraint for the generic type T
, which gives us a compile-time guarantee that both arguments will be of the exact same type:
func valueDidChange<T: Equatable>(from old: T, to new: T) {
guard old != new else {
return
}
...
}
So when Self
is being used within a protocol, the compiler needs to know exactly what type that we’re referring to within each given context. To make that happen, we can either use a conforming concrete type directly (such as using Int
or String
, instead of Equatable
), or use the protocol as a generic constraint (like above).
The same thing is also true for protocols that have associated types, since just like Self
, the actual types that are being referred to won’t be known unless we provide some additional context. For example, here we’ve defined a Loadable
protocol that enables us to load an associated Target
type through a load()
method:
protocol Loadable {
associatedtype Target
func load() -> Target
}
What exact type that Target
will refer to will vary depending on what conforming type that we’re calling load()
on, so something like the following wouldn’t work — since the compiler can’t tell what type that’s being loaded:
func loadTarget(from loadable: Loadable) {
let target = loadable.load()
...
}
Just like before, we can solve the above problem by using a generic type constraint to give the compiler awareness of what exact Loadable
implementation that each call site is using:
func loadTarget<T: Loadable>(from loadable: T) {
let target = loadable.load()
...
}
Now, whenever we’re calling the above loadTarget
function, the compiler will be able to infer what type that T
refers to based on the Loadable
type that’s being passed into our function. For example, here the compiler can infer that T
means StringLoader
and that the Target
being loaded is a String
value:
struct StringLoader: Loadable {
func load() -> String {
...
}
}
let loader = StringLoader()
loadTarget(from: loader)
There are also a number of other techniques that can be really useful when dealing with generic protocols — including type erasure, using more advanced constraints, and more. Feel free to explore the category pages for generics and protocols to learn more.