Equality and Comparison with custom objects in Swift (Equatable protocol)

Few weeks ago I ran into frustrating problem. I was dealing with custom objects in Swift. (Actually struct with properties). They were being stored in an array. Now, this array was going to undergo frequent activity due to user action such as adding new elements and avoiding duplicates. So while adding elements to an array my naive approach was to iterate through an array and check individual element for equality. If there is a match do not add it and continue. But if object is new, add it to an array.

Let's make one such object Student. It will have 3 fields, viz. rank, subRank and name. These properties will be used in rest part of the article to demonstrate equality and comparison operators.


struct Student {
    var rank: Int
    var subRank: Int
    var name: String
}
// Naive approach

func insert(student: Student) {
    var studentExists: Bool = false
    for stdnt in self.studentsCollection {
        if stdnt.rank == student.rank && stdnt.subRank == student.subRank && stdnt.name == student.name {
            studentExists = true
            break
        }
    }

    if !studentExists {
        self.studentsCollection.append(student)
    }
}

As it's evident from example above, we iterate over every student in the list and if no exact match is found for all student, studentExists is still false at the end and we insert the student object at the end of iteration.

However, as naive as it looks, our worst case time complexity is O(n) every time we try to insert a student object. Swift offers a better solution over this.

For any custom object to provide better equatable approach, custom object may conform to Equatable protocol. By doing this it can add an extension function which adds static operator function for == operator.

This is useful since you can directly use array.contains(element) method to check the existence of element in an array without having to compare objects manually. Every time you execute array.contains([custom object]), the static operator function for == is called and it internally checks if array already has custom object by performing conditional check provided by operator function


extension Student: Equatable {
    static func == (lhs: Student, rhs: Student) -> Bool {
        return lhs.rank == rhs.rank && lhs.subRank == rhs.subRank && lhs.name == rhs.name
    }
}
// Now having declared custom static function on operator `==`, you can directly do following to check the existence of element
func insert(student: Student) {
    if !self.studentsCollection.contains(student) {
        self.studentsCollection.append(student)
    }
}

Pretty sweet, huh?

Next is comparison operator. Let's use the same example with Student object.

Generally you would use following code with built-in sorted function to sort custom operator based on the logic of sorting.


let student1 = Student(rank: 300, subRank: 9, name: "jay1")
let student2 = Student(rank: 100, subRank: 9, name: "jay3")
let student3 = Student(rank: 900, subRank: 9, name: "jay2")

studentsCollection = [student1, student2, student3]

let sortedCollection = studentsCollection.sorted(by: { ($0.rank < $1.rank) || ($0.rank == $1.rank && $0.subRank < $1.subRank) && ($0.rank == $1.rank && $0.subRank == $1.subRank && $0.name < $1.name) })

Although this works, there is one more way to separate this logic from actual codebase. You can declare a static operator function in the same extension of Equatable protocol for < operator.


extension Student: Equatable {
    static func < (lhs: Student, rhs: Student) -> Bool {
        return (lhs.rank < rhs.rank) || (lhs.rank == rhs.rank && lhs.subRank < rhs.subRank) && (lhs.rank == rhs.rank && lhs.subRank == rhs.subRank && lhs.name < rhs.name)
    }
}

// Now having declared it as a part of Equatable extension, you can directly call `sorted` on an array without having to specify operator equality individually.

let sortedCollection = studentsCollection.sorted(by: <) < code>

You can use similar technique for > operator as well.

As a part of summary I would say making comparison and equality logic as a part of Equatable protocol allows us to separate these two pieces of logic from main codebase. This is great for testing as well since you can easily override the behavior in unit testing class.

References: