Checking whether two objects or values are considered equal is definitely one of the most commonly performed operations in all of programming. So, in this article, let’s take a look at how Swift models the concept of equality, and how that model varies between value and reference types.
One the most interesting aspects of Swift’s implementation of equality is that it’s all done in a very protocol-oriented way — meaning that any type can become equatable by conforming to the Equatable
protocol, which can be done like this:
struct Article: Equatable {
static func ==(lhs: Self, rhs: Self) -> Bool {
lhs.title == rhs.title && lhs.body == rhs.body
}
var title: String
var body: String
}
The way that we conform to Equatable
in the above example is by implementing an overload of the ==
operator, which accepts the two values to compare (lhs
, the left-hand side value, and rhs
, the right-hand side value), and it then returns a boolean result as to whether those two values should be considered equal.
The good news, though, is that we typically don’t have to write those kinds of ==
operator overloads ourselves, since the compiler is able to automatically synthesize such implementations whenever a type’s stored properties are all Equatable
themselves. So in the case of the above Article
type, we can actually remove our manual equality checking code, and simply make that type look like this:
struct Article: Equatable {
var title: String
var body: String
}
The fact that Swift’s equality checks are so protocol-oriented also gives us a ton of power when working with generic types. For example, a collection of Equatable
-conforming values (such as an Array
or Set
) are automatically considered equatable as well — without requiring any additional code on our part:
let latestArticles = [
Article(
title: "Writing testable code when using SwiftUI",
body: "..."
),
Article(title: "Combining protocols in Swift", body: "...")
]
let basicsArticles = [
Article(title: "Loops", body: "..."),
Article(title: "Availability checks", body: "...")
]
if latestArticles == basicsArticles {
...
}
The way that those kinds of collection equality checks work is through Swift’s conditional conformances feature, which enables a type to conform to a specific protocol only when certain conditions are met. For example, here’s how Swift’s Array
type conforms to Equatable
only when the elements that are being stored within a given array are also, in turn, Equatable
-conforming — which is what makes it possible for us to check whether two Article
arrays are considered equal:
extension Array where Element: Equatable {
...
}
Since none of the above logic is hard-coded into the compiler itself, we can also utilize that exact same conditional conformances-based technique if we wanted to make our own generic types conditionally equatable as well. For example, our code base might include some form of Group
type that can be used to label a group of related values:
struct Group<Value> {
var label: String
var values: [Value]
}
To make that Group
type conform to Equatable
when it’s being used to store Equatable
values, we simply have to write the following empty extension, which looks almost identical to the Array
extension that we took a look at above:
extension Group: Equatable where Value: Equatable {}
With the above in place, we can now check whether two Article
-based Group
values are equal, just like we could when using arrays:
let latestArticles = Group(
label: "Latest",
values: [
Article(
title: "Writing testable code when using SwiftUI",
body: "..."
),
Article(title: "Combining protocols in Swift", body: "...")
]
)
let basicsArticles = Group(
label: "Basics",
values: [
Article(title: "Loops", body: "..."),
Article(title: "Availability checks", body: "...")
]
)
if latestArticles == basicsArticles {
...
}
Just like collections, Swift tuples can also be checked for equality whenever their stored values are all Equatable
-conforming:
let latestArticles = (
first: Article(
title: "Writing testable code when using SwiftUI",
body: "..."
),
second: Article(title: "Combining protocols in Swift", body: "...")
)
let basicsArticles = (
first: Article(title: "Loops", body: "..."),
second: Article(title: "Availability checks", body: "...")
)
if latestArticles == basicsArticles {
...
}
However, collections containing the above kind of equatable tuples do not automatically conform to Equatable
. So, if we were to put the above two tuples into two identical arrays, then those wouldn’t be considered equatable:
let firstArray = [latestArticles, basicsArticles]
let secondArray = [latestArticles, basicsArticles]
if firstArray == secondArray {
...
}
The reason why the above doesn’t work (at least not out of the box) is because — like the emitted compiler message alludes to — tuples can’t conform to protocols, which means that the Equatable
-conforming Array
extension that we took a look at earlier won’t take effect.
There is a way to make the above work, though, and while I realize that the following generic code might not belong in an article labeled as ”Basics”, I still thought it would be worth taking a quick look at — since it illustrates just how flexible Swift’s equality checks are, and that we’re not just limited to implementing a single ==
overload in order to conform to Equatable
.
So if we were to add another, custom ==
overload, specifically for arrays that contain equatable two-element tuples, then the above code sample will actually compile successfully:
extension Array {
static func ==<A: Equatable, B: Equatable>(
lhs: Self,
rhs: Self
) -> Bool where Element == (A, B) {
guard lhs.count == rhs.count else {
return false
}
return zip(lhs, rhs).allSatisfy(==)
}
}
Above we can also see how Swift operators can be passed as functions, since we’re able to pass ==
directly to our call to allSatisfy
.
So far, we’ve been focusing on how value types (such as structs) behave when checked for equality, but what about reference types? For example, let’s say that we’ve now decided to turn our previous Article
struct into a class instead, how would that impact its Equatable
implementation?
class Article: Equatable {
var title: String
var body: String
init(title: String, body: String) {
self.title = title
self.body = body
}
}
The first thing that we’ll notice when performing the above change is that the compiler is no longer able to automatically synthesize our type’s Equatable
conformance — since that feature is limited to value types. So if we wanted our Article
type to remain a class, then we’d have to manually implement the ==
overload that Equatable
requires, just like we did at the beginning of this article:
class Article: Equatable {
static func ==(lhs: Article, rhs: Article) -> Bool {
lhs.title == rhs.title && lhs.body == rhs.body
}
var title: String
var body: String
init(title: String, body: String) {
self.title = title
self.body = body
}
}
However, classes that are subclasses of any kind of Objective-C-based class do inherit a default Equatable
implementation from NSObject
(which is the root base class for almost all Objective-C classes). So, if we were to make our Article
class an NSObject
subclass, then it would actually become Equatable
without strictly requiring us to implement a custom ==
overload:
class Article: NSObject {
var title: String
var body: String
init(title: String, body: String) {
self.title = title
self.body = body
super.init()
}
}
While it might be tempting to use the above subclassing technique to avoid having to write custom equality checking code, it’s important to point out that the only thing that the default Objective-C-provided Equatable
implementation will do is to check if two classes are the same instance — not if they contain the same data. So even though the following two Article
instances have the same title
and body
, they won’t be considered equal when using the above NSObject
-based approach:
let articleA = Article(title: "Title", body: "Body")
let articleB = Article(title: "Title", body: "Body")
print(articleA == articleB)
Performing those kinds of instance checks can be really useful, though — as sometimes we might want to be able to check whether two class-based references point to the same underlying instance. We don’t need our classes to inherit from NSObject
to do that, though, since we can use Swift’s built-in triple-equals operator, ===
, to perform such a check between any two references:
let articleA = Article(title: "Title", body: "Body")
let articleB = articleA
print(articleA === articleB)
To learn more about the above concept, check out “Identifying objects in Swift”.
With that, I believe that we’ve covered all of the basics as to how equality works in Swift — for both values and objects, using either custom or automatically generated implementations, as well as how generics can be made conditionally equatable. If you have any questions, comments, or feedback, then feel free to reach out via either Twitter or email.
Thanks for reading!