Articles, podcasts and news about Swift development, by John Sundell.

Equality

Published on 04 Mar 2022

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]

// Compiler error: Type '(first: Article, second: Article)'
// cannot conform to 'Equatable':
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 {
    // This '==' overload will be used specifically when two
    // arrays containing two-element tuples are being compared:
    static func ==<A: Equatable, B: Equatable>(
        lhs: Self,
        rhs: Self
    ) -> Bool where Element == (A, B) {
        // First, we verify that the two arrays that are being
        // compared contain the same amount of elements:
        guard lhs.count == rhs.count else {
            return false
        }

        // We then "zip" the two arrays, which will give us
        // a collection where each element contains one element
        // from each array, and we then check that each of those
        // elements pass a standard equality check:
        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) // false

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) // true

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!