Check if two values of type Any are equal
In Swift 5.7 that comes with Xcode 14 we can more easily check if two values of type Any
are equal, because we can cast values to any Equatable
and also use any Equatable
as a parameter type thanks to Unlock existentials for all protocols change.
We can start by extending Equatable
protocol with isEqual()
method that accepts another any Equatable
as an argument.
extension Equatable {
func isEqual(_ other: any Equatable) -> Bool {
guard let other = other as? Self else {
return false
}
return self == other
}
}
Inside isEqual()
method we have access to Self
, so we can try to cast the other
value to the same type as the concrete type of the value this method is called on. If the cast fails, the two values cannot be equal, so we return false
. If it succeeds, then we can check the two values for equality, using ==
function on values of the same concrete type.
If we don't use this method with class subtypes, it will work well. However, it won't always provide consistent results when checking a child and a parent class for equality, as Tikitu de Jager pointed out in his X comment.
Our isEqual()
method can be improved by adding the inverse check, and David Peterson suggested a great solution for that, also on X.
extension Equatable {
func isEqual(_ other: any Equatable) -> Bool {
guard let other = other as? Self else {
return other.isExactlyEqual(self)
}
return self == other
}
private func isExactlyEqual(_ other: any Equatable) -> Bool {
guard let other = other as? Self else {
return false
}
return self == other
}
}
Note, that there are still some edge cases that are not covered, like checking two sibling classes for equality. And as Becca Royal-Gordon noted in her X comment, this won't treat unbridged and bridged Foundation types as equal, like AnyHashable
does. But as long as we are aware of the limitations, we can still use this solution in our projects.
Now we can define a global areEqual()
function that will compare two values of type Any
. Unfortunately, we can't' extend Any
type, so it will have to be a global function.
func areEqual(first: Any, second: Any) -> Bool {
guard
let equatableOne = first as? any Equatable,
let equatableTwo = second as? any Equatable
else { return false }
return equatableOne.isEqual(equatableTwo)
}
Inside areEqual()
we can try to cast both of Any
values to any Equatable
and if we succeed, we can call our previously defined isEqual()
method on one of the values, passing the other values as an argument.
Overloading ==
operator for two values of type Any
seems to be causing a run-time crash because it enters a recursive loop in Swift at the moment, even if I put @_disfavoredOverload
on it. When we have different overloads with parameters of concrete types and protocols, Swift can't always disambiguate them. @_disfavoredOverload
should usually help with that, but it didn't work in this case.
Another way to overload ==
operator would be to set generic parameters without constraints, so that Swift doesn't choose this version when it has two concrete types. This will let us compare two Any
values as well.
func ==<L, R>(lhs: L, rhs: R) -> Bool {
guard
let equatableLhs = lhs as? any Equatable,
let equatableRhs = rhs as? any Equatable
else { return false }
return equatableLhs.isEqual(equatableRhs)
}
Now we can use our areEqual()
function or ==
overload, when we need to compare two values of type Any
. For example, we might want to check if two dictionaries of NSAttributedString
attributes are equal without casting them to NSDictionary
.
typealias NSAttributes = [NSAttributedString.Key: Any]
func ==(lhs: NSAttributes, rhs: NSAttributes) -> Bool {
if lhs.isEmpty && rhs.isEmpty { return true }
guard lhs.count == rhs.count else { return false }
for (key, lhsValue) in lhs {
guard let rhsValue = rhs[key] else {
return false
}
if !areEqual(first: lhsValue, second: rhsValue) {
return false
}
}
return true
}
As someone who has worked extensively with Swift, I've gathered many such insights over the years. I'm excited to share these in my new book Swift Gems. This book is packed with advanced tips and techniques to help intermediate and advanced Swift developers enhance their coding skills. From optimizing collections and handling strings to mastering asynchronous programming and debugging, "Swift Gems" provides practical advice to elevate your Swift development. Grab your copy of Swift Gems and let's explore the advanced techniques together.