Is there way to define compare (`==`) function automatically for `struct` in Swift?

2.5k Views Asked by At

Let's assume we have a pretty big struct in Swift:

struct SuperStruct {
    var field1: Int = 0
    var field2: String = ""
    // lots of lines...
    var field512: Float = 0.0
}

.. and then we need to implement Equatable protocol:

extension SuperStruct: Equatable {
}

func ==(lhs: SuperStruct, rhs: SuperStruct) -> Bool {
    return
        lhs.field1 == rhs.field1 &&
        lhs.field2 == rhs.field2 &&
        // lots of lines...
        lhs.field512 == rhs.field512
}

... and we need to write lots of lines of stupid code. Is there a way "to ask" compiler "to do" it for us?

4

There are 4 best solutions below

0
On BEST ANSWER

In Swift 4.1, Equatable/Hashable types now synthesize conformance to Equatable/Hashable if all of the types' members are Equatable/Hashable

SE-0185

Synthesizing Equatable and Hashable conformance

Developers have to write large amounts of boilerplate code to support equatability and hashability of complex types. This proposal offers a way for the compiler to automatically synthesize conformance to Equatable and Hashable to reduce this boilerplate, in a subset of scenarios where generating the correct implementation is known to be possible.

https://github.com/apple/swift-evolution/blob/master/proposals/0185-synthesize-equatable-hashable.md

3
On

The following answer shows one possible solution; possibly not a recommended one (however possibly of interest for future readers of this question).


If you have a large number of properties which all belong to a somewhat limited of number different types, you could use a Mirror of your structure instances and iterate over over the structures' properties; for each attempting conversion to the different types that you know your properties to be.

I've edited the previous answer (to something I believe is quite much neater), after watching the following WWDC 2015 session (thanks Leo Dabus!):

I'll leave the initial answer in the bottom of this answer as well, as it shows an alternative, less protocol-oriented approach, to make use of this Mirror solution.

Mirror & protocol-oriented solution:

/* Let a heterogeneous protocol act as "pseudo-generic" type
   for the different (property) types in 'SuperStruct'         */
protocol MyGenericType {
    func isEqualTo(other: MyGenericType) -> Bool
}
extension MyGenericType where Self : Equatable {
    func isEqualTo(other: MyGenericType) -> Bool {
        if let o = other as? Self { return self == o }
        return false
    }
}

/* Extend types that appear in 'SuperStruct' to MyGenericType  */
extension Int : MyGenericType {}
extension String : MyGenericType {}
extension Float : MyGenericType {}
    // ...

/* Finally, 'SuperStruct' conformance to Equatable */
func ==(lhs: SuperStruct, rhs: SuperStruct) -> Bool {

    let mLhs = Mirror(reflecting: lhs).children.filter { $0.label != nil }
    let mRhs = Mirror(reflecting: rhs).children.filter { $0.label != nil }

    for i in 0..<mLhs.count {
        guard let valLhs = mLhs[i].value as? MyGenericType, valRhs = mRhs[i].value as? MyGenericType else {
            print("Invalid: Properties 'lhs.\(mLhs[i].label!)' and/or 'rhs.\(mRhs[i].label!)' are not of 'MyGenericType' types.")
            return false
        }
        if !valLhs.isEqualTo(valRhs) {
            return false
        }
    }
    return true
}

Example usage:

/* Example */
var a = SuperStruct()
var b = SuperStruct()
a == b // true
a.field1 = 2
a == b // false
b.field1 = 2
b.field2 = "Foo"
a.field2 = "Foo"
a == b // true

Previous Mirror solution:

/* 'SuperStruct' conformance to Equatable */
func ==(lhs: SuperStruct, rhs: SuperStruct) -> Bool {

    let mLhs = Mirror(reflecting: lhs).children.filter { $0.label != nil }
    let mRhs = Mirror(reflecting: rhs).children.filter { $0.label != nil }

    for i in 0..<mLhs.count {
        switch mLhs[i].value {
        case let valLhs as Int:
            guard let valRhs = mRhs[i].value as? Int where valRhs == valLhs else {
                return false
            }
        case let valLhs as String:
            guard let valRhs = mRhs[i].value as? String where valRhs == valLhs else {
                return false
            }
        case let valLhs as Float:
            guard let valRhs = mRhs[i].value as? Float where valRhs == valLhs else {
                return false
            }
            /* ... extend with one case for each type
            that appear in 'SuperStruct'  */
        case _ : return false
        }
    }
    return true
}

Example usage:

/* Example */
var a = SuperStruct()
var b = SuperStruct()
a == b // true
a.field1 = 2
a == b // false
b.field1 = 2
b.field2 = "Foo"
a.field2 = "Foo"
a == b // true
0
On

No, it doesn't. At least not in any way that's not excessively complicated and based on use (abuse?) of runtime introspection. See dfri's answer for something that technically works, but that is way more complicated than just writing an == implementation that directly compares all fields.

As for your opinions on what "should" be available in Swift, you're more likely to see some effect if you share them with Apple or with the Swift open source community.

0
On

You could make the struct Codable and compare the JSON encoded Data. Not efficient, but could be useful for some applications (e.g. unit tests).

struct SuperStruct: Encodable {
    var field1: Int = 0 
    // ....
    var field512: Float = 0.0
}

let s1 = SuperStruct()
let s2 = SuperStruct()

let encoder = JSONEncoder()
let data1 = try! encoder.encode(s1)
let data2 = try! encoder.encode(s2)
let result = (data1 == data2)

If you like this you could tidy it up into a protocol extension of Encodable.