Reputation: 4330
I have a protocol:
protocol CustomProtocol {
var title: String { get }
var subtitle: String { get }
}
Then i have 2 objects, that conform this procotol. And i want to compare them, so i would like to CustomProtocol to be Equatable.
protocol CustomProtocol: Equatable {
var title: String { get }
var subtitle: String { get }
static func ==(lhs: Self, rhs: Self) -> Bool
}
extension CustomProtocol {
static func ==(lhs: Self, rhs: Self) -> Bool {
return lhs.title == rhs.title
}
}
But after that change i get "Protocol CustomProtocol can only be used as a generic constraint because it has Self or associated type requeriments. The only way i can think to solve this is to have a third property like a hash that depends on the others and compare this property.
Here you have a sample playground with the actual code.
Upvotes: 5
Views: 4304
Reputation: 1
The problem is the rhs parameter is not the same type of the lhs. They just conform to the same protocol.
You could solve that by using a generic type as the second parameter.
exension CustomProtocol {
static func ==<T: CustomProtocol>(lhs: Self, rhs: T) -> Bool {
return lhs.title == rhs.title
}
}
Upvotes: -1
Reputation: 766
Because Equatable
has Self
requirements, it should not be implemented directly on a protocol. Otherwise, the protocol will be unusable as a type.
To implement Equatable
at the protocol level yet be able to use the protocol as a type, you can use type erasure.
To demonstrate, I have modified the code given in your playground to build a type eraser.
For a detailed explanation of the approach I have used, check out this post on my blog:
https://khawerkhaliq.com/blog/swift-protocols-equatable-part-two/
Here is the modified code from your playground:
protocol CustomProtocol {
var title: String { get }
var subtitle: String { get }
func isEqualTo(_ other: CustomProtocol) -> Bool
func asEquatable() -> AnyEquatableCustomProtocol
}
extension CustomProtocol where Self: Equatable {
func isEqualTo(_ other: CustomProtocol) -> Bool {
guard let o = other as? Self else { return false }
return self == o
}
func asEquatable() -> AnyEquatableCustomProtocol {
return AnyEquatableCustomProtocol(self)
}
}
struct A: CustomProtocol, Equatable {
var title: String
var subtitle: String
static func ==(lhs: A, rhs: A) -> Bool {
return lhs.title == rhs.title && lhs.subtitle == rhs.subtitle
}
}
struct B: CustomProtocol, Equatable {
var title: String
var subtitle: String
static func ==(lhs: B, rhs: B) -> Bool {
return lhs.title == rhs.title && lhs.subtitle == rhs.subtitle
}
}
struct AnyEquatableCustomProtocol: CustomProtocol, Equatable {
var title: String { return value.title }
var subtitle: String { return value.subtitle }
init(_ value: CustomProtocol) { self.value = value }
private let value: CustomProtocol
static func ==(lhs: AnyEquatableCustomProtocol, rhs: AnyEquatableCustomProtocol) -> Bool {
return lhs.value.isEqualTo(rhs.value)
}
}
// instances typed as the protocol
let a: CustomProtocol = A(title: "First title", subtitle: "First subtitle")
let b: CustomProtocol = B(title: "First title", subtitle: "First subtitle")
let equalA: CustomProtocol = A(title: "First title", subtitle: "First subtitle")
let unequalA: CustomProtocol = A(title: "Second title", subtitle: "Second subtitle")
// equality tests
print(a.asEquatable() == b.asEquatable()) // prints false
print(a.asEquatable() == equalA.asEquatable()) // prints true
print(a.asEquatable() == unequalA.asEquatable()) // prints false
The point to note is that with this approach the actual ==
comparisons are delegated to the underlying concrete types, yet we deal only with protocol types to maintain abstraction.
Here I have used the type erased instances only for one comparison. However, since the type eraser conforms to CustomProtocol
these instances can be saved and used in any place where a protocol type is expected. Because they conform to Equatable
, they can also be used in any place where Equatable
conformance is required.
Just for context, this post explains why it is not advisable to try to implement Equatable
conformance or even ==
functions directly on protocols:
https://khawerkhaliq.com/blog/swift-protocols-equatable-part-one/
Hence the type erasure.
Hope this helps.
Upvotes: 3
Reputation: 12109
The Equatable protocol has a self constraint to solve the problem that you only should be able to check equality between objects of the same type, not the same protocol. That's why it has a self-requirement. Otherwise you could just say
let a: Equatable = 42
let b: Equatable = "hello"
and a == b
would work. This would be bad because you could compare objects of totally unrelated types. The self-requirement makes this a compile time error.
If you want to compare your objects on a protocol basis, just implement the ==
operator without a self-requirement:
extension CustomProtocol {
func == (lhs: CustomProtocol, rhs: CustomProtocol) -> Bool {
return lhs.name == rhs.name
}
func != (lhs: CustomProtocol, rhs: CustomProtocol) -> Bool {
return !(lhs == rhs)
}
}
Now you can declare instances of your protocol directly with the CustomProtocol
type and compare them.
But maybe the protocol is not the right abstraction in this case. Maybe you should implement this as an abstract class.
Upvotes: 2