Reputation: 32549
I'm trying to implement a cache for my entities with using generics in Swift. Here is my code:
class BaseCache<T>: NSObject {
var allEntities = [T]()
// MARK: - Append
func appendEntities(newEntities: [T]) {
for entity in newEntities {
// Check if allEntities array already contains an entity
var contains = false
for item in allEntities {
// EXC_BAD_ACCESS in isEqual method (see below)
if isEqual(entity, rightEntity: item) {
contains = true
break
}
}
if !contains {
allEntities.append(entity)
}
}
}
func isEqual(leftEntity: T, rightEntity: T) -> Bool {
return false
}
}
Here is a concrete implementation of BaseCache:
class CourierCache<T: AftershipCourier>: BaseCache<T> {
override func isEqual(leftEntity: T, rightEntity: T) -> Bool {
println("\(leftEntity)") // EXC_BAD_ACCESS here
println("\(rightEntity)")
return rightEntity.slug == leftEntity.slug
}
}
Any ideas how to fix that? Thanks!
PS: Note that this question is not relevant to my question
Upvotes: 2
Views: 437
Reputation: 40973
Looks to me like you’ve found a Swift bug. Here’s as simple as I could get it:
class C { }
class Base<T> {
func callCrash(t: T) {
crash(t)
}
func crash(t: T) { }
}
class Sub<T: C>: Base<T> {
override func crash(t: T) {
println(t) // EXC_BAD_ACCESS here
}
}
let sub = Sub<C>()
sub.callCrash(C())
However, you would probably be better served by putting the ability to detect equality into a protocol, and then requiring the objects, rather than the cache, to check for equality.
@rakeshbs’s answer shows how to do this with Equatable
, but I would add a couple of caveats that means you may not want to use this approach:
You are checking a property, slug
, to test for equality. Equality in Swift implies substitutability – i.e. if two elements are equal via ==
, they should be completely equivalent and you should be able to substitute one for the other without anyone noticing. If your ships have properties that can vary even while their slug
property is the same, this will not be the case. This can lead to some nasty bugs if you use library functions like contains
or sort
that rely on this substitutability property. If you are using classes, then you might find the identity operator (===
) is a good thing to use to implement the equality operator.
Using equatable and ==
operators and generics means your comparison function will be statically bound, because operators are not member functions. That means if you hold in your cache different objects in the hierarchy, you won't get dynamic dispatch on your ==
operator. That is, if you have an AftershipCourier
cache and you put FastAftershipCourier
classes in it, you could find that the ==
for AftershipCourier
be run between them, instead of a custom ==
that compares FastAftershipCourier
. So if you need dynamic behaviour, make sure to have ==
call a method on the passed-in argument, that can be overridden by subclasses, rather than just comparing properties directly.
To resolve both these issues, use a protocol of your own devising with a comparison function, have the courier classes implement it, and then call it within your cache code.
P.S. your for loop checking the entity against allEntities can be written as let alreadyContained = contains(allEntities) { entity.comparingFuncYouDecideOn($0) }
Upvotes: 1
Reputation: 24572
You can modify your code like this to achieve what you need. Make use of the Equatable protocol to compare the AfterCourier instances. And use type alias to fix the type inside CourierCache.
class BaseCache<T:Equatable> {
var allEntities :Array<T> = []
func appendEntities(newEntities: [T])
{
for entity in newEntities {
if !contains(allEntities,entity)
{
allEntities.append(entity)
}
}
}
func print()
{
println("Entities : \(allEntities)")
}
}
class CourierCache<S>: BaseCache<AftershipCourier>
{
func testCourier()
{
for courier in allEntities
{
println("Courier Slug: \(courier.slug)")
}
}
}
class AftershipCourier : Equatable,Printable
{
var description: String {
return "Courier Slug: \(slug)"
}
var slug : Int = 0
}
func == (lhs: AftershipCourier, rhs: AftershipCourier) -> Bool
{
return lhs.slug == rhs.slug
}
typealias AfterCourierCache = CourierCache<AftershipCourier>
You can use it like this.
var cache = CourierCache<AftershipCourier>()
var t1 = AftershipCourier()
t1.slug = 1
var t2 = AftershipCourier()
t2.slug = 2
cache.appendEntities([t1])
cache.appendEntities([t2])
cache.print()
cache.testCourier();
Upvotes: 0