Let's start with your CustomEquatable protocol, without the extension:
protocol CustomEquatable {
func isEqualTo(_ other: CustomEquatable) -> Bool
}
Let's define some types to use for experiments:
struct A: Equatable {
let name: String
}
struct B: Equatable {
let id: Int
}
Suppose we then want A and B to conform to CustomEquatable. Then we have four cases to consider:
- What does
a1.isEqualTo(a2) mean (where a1 and a2 are both of type A)?
- What does
b1.isEqualTo(b2) mean (where b1 and b2 are both of type B)?
- What does
a.isEqualTo(b) mean (where a is an A and b is a B)?
- What does
b.isEqualTo(a) mean (where b is a B and a is an A)?
For the first two cases, possible answers are that a1.isEqualTo(a2) if and only if a1 == a2 and b1.isEqualTo(b2) if and only if b1 == b2.
For the second two cases, we have to decide if there's a way for an A to equal a B. The simplest solution (I think) is that an A can never equal a B.
So we can write the conformances like this:
extension A: CustomEquatable {
func isEqualTo(_ other: CustomEquatable) -> Bool {
return (other as? A) == self
}
}
extension B: CustomEquatable {
func isEqualTo(_ other: CustomEquatable) -> Bool {
return (other as? B) == self
}
}
The only difference in these two conformances is the cast-to type (on the right side of as?). So we can factor out the conformances into a protocol extension like this:
extension CustomEquatable where Self: Equatable {
func isEqualTo(_ other: CustomEquatable) -> Bool {
return (other as? Self) == self
}
}
With this protocol extension, we can make A and B conform to CustomEquatable without implementing isEqualTo for each:
extension A: CustomEquatable { }
extension B: CustomEquatable { }
To test the code:
let a1 = A(name: "a1")
let a2 = A(name: "a2")
let b1 = B(id: 1)
let b2 = B(id: 2)
a1.isEqualTo(a1) // true
a1.isEqualTo(a2) // false
b1.isEqualTo(b1) // true
b1.isEqualTo(b2) // false
a1.isEqualTo(b1) // false
b1.isEqualTo(a1) // false