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