somu
(somu)
1
Overview:
- I have a
Shape protocol
- I have
Circle and Square that conform to Shape
- I would like to store different Shapes in an array
- I would like a way to compare the elements of the array for equality
Problem:
- I could make them conform to
Equatable but then I wouldn't be able to store the Heterogeneous collection.
- In the real project, I am using CoreData, so couldn't use
enum with associated values for the Shape and use it for comparison.
Questions:
- I have made an attempt, just wondering if there was a better solution?
- Is there a way to use == or any other operator? (based on my attempt, seemed like all of them required to be aware of
Self)
- Is my attempt reasonable?
My Attempt
protocol Shape {
var name : String { get }
var size : Int { get }
}
extension Shape {
func isEqual(to rhs: Shape) -> Bool {
name == rhs.name && size == rhs.size
}
static func == (lhs: Shape, rhs: Shape) -> Bool {
lhs.name == rhs.name && lhs.size == rhs.size
}
}
struct Circle : Shape {
let name = "Circle"
var size : Int
}
struct Square : Shape {
let name = "Square"
var size : Int
}
let s1 = Square(size: 3)
let c1 = Circle(size: 3)
var shapes : [Shape] = [s1, c1]
s1.isEqual(to: c1) //works ok
s1 == c1 //compilation error: Generic parameter 'Self' could not be inferred
cukr
2
it works if you make it a free function instead of a static method
func == (lhs: Shape, rhs: Shape) -> Bool {
lhs.name == rhs.name && lhs.size == rhs.size
}
Are you okay with the fact that two different types, but with the same name and size would be considered equal?
Are you okay with the fact that if you add a third property to a Circle it wouldn't be considered when comparing two circles?
Are you okay with Shape not being equatable itself (for example you cannot compare two arrays of Shapes)
If you're okay with all these disadvantages, then it's reasonable.
Usually the solution is to make a type-erasing wrapper, check out AnyHashable, or AnyView in SwifUI (There are multiple ways to write a type-erasing wrapper, using a closure is the easiest one)
protocol Shape: Equatable {
var name : String { get }
var size : Int { get }
}
struct AnyShape: Shape {
let base: Any // you don't have to provide this property, but it's often handy
let name: String
let size: Int
private let compare: (Any) -> Bool
init<T: Shape>(_ shape: T) {
self.base = shape
self.name = shape.name
self.size = shape.size
self.compare = {
guard let other = $0 as? T else { return false }
return shape == other
}
}
static func == (lhs: AnyShape, rhs: AnyShape) -> Bool {
lhs.compare(rhs.base)
}
}
struct Circle : Shape {
let name = "Circle"
var size : Int
}
struct Square : Shape {
let name = "Square"
var size : Int
}
let s1 = Square(size: 3)
let c1 = Circle(size: 3)
var shapes = [AnyShape(s1), AnyShape(c1)]
shapes == shapes
AnyShape(s1) == AnyShape(c1)
4 Likes
Lantua
3
If you only compare a fixed subset of variables (that every Shape shares), I'd suggest that you make an id property for that comparison.
extension Shape {
struct ID: Hashable {
var name: String
var size: Int
}
var id: ID { .init(...) }
}
shape1.id == shape2.id
3 Likes
somu
(somu)
4
Thanks a lot @cukr
Pretty cool that you can create a stand alone == function and that would eliminate the problem of needing to be aware of Self
I think you made valid points about the limitations with my approach, I think the biggest limitation would be array comparison, I might need to build a == function for Sequence.
I am hardcoding name and making it let so that part is ok.
1 Like
somu
(somu)
5
Thanks a lot @Lantua
I think that is a good idea to compare based ID, makes it a lot simpler to compare without the burden of being aware of the type of Shape.