7

After upgrading our codebase to Swift2 I've encountered unusual problem. Set is not substracting nor unioning as expected.

class A: NSObject {
    let h: Int

    init(h: Int) {
        self.h = h
    }

    override var hashValue: Int {
        return h
    }
}

func ==(lhs: A, rhs: A) -> Bool {
    return lhs.hashValue == rhs.hashValue
}

let a = A(h: 1)
let b = A(h: 1)

var sa = Set([a])
let sb = Set([b])

sa.subtract(sb).count // Swift1.2 prints 0, Swift 2 prints 1

sa.contains(a) // Swift1.2 true, Swift 2 true
sa.contains(b) // Swift1.2 true, Swift 2 false

It looks like new Set is not using hashValue for internal operations. Any idea is that a bug, or a way to workaround this issue?

muvaaa
  • 450
  • 4
  • 12

2 Answers2

12

I played with your code a bit. I was able to get it working by no longer subclassing NSObject, but instead conforming to the Hashable protocol:

class A: Hashable {
    let h: Int

    init(h: Int) {
        self.h = h
    }

    var hashValue: Int {
        return h
    }

}

func ==(lhs: A, rhs: A) -> Bool {
    return lhs.hashValue == rhs.hashValue
}

let a = A(h: 1)
let b = A(h: 1)

var sa = Set([a])
let sb = Set([b])

sa.subtract(sb).count // Swift1.2 prints 0, Swift 2 prints 1

sa.contains(a) // Swift1.2 true, Swift 2 true
sa.contains(b) // Swift1.2 true, Swift 2 false

a.hashValue == b.hashValue

When you were inheriting from NSObject, your == overload wasn't actually being executed. If you want this to work with NSObject, you'd have to override isEquals:

override func isEqual(object: AnyObject?) -> Bool {
    if let object = object as? A {
        return object.h == self.h
    } else  {
        return false
    }
}
TheRobDay
  • 511
  • 3
  • 7
  • Thank you! I was having this problem with a MKAnnotation subclass (which also needs to extend NSObject). Do you have a link to some documentation on the subject? – brki Oct 06 '15 at 20:58
  • 2
    Both isEqual and hashValue seems to be needed to make Set correctly with NSObject in Swift 3 – Morten Holmgaard Jan 28 '17 at 13:30
4
//: Playground - noun: a place where people can play

import Foundation

class X: NSObject {
    var x:String
    var y:Int

    init(x:String, y:Int) {
        self.x = x
        self.y = y
        super.init()
    }

    override var hashValue: Int {
        return x.hashValue ^ y.hashValue
    }

    override func isEqual(object: AnyObject?) -> Bool {
        if let rhs = object as? X {
            return x == rhs.x && y == rhs.y
        } else {
            return false
        }
    }
}

func == (lhs:X, rhs:X) -> Bool {
    return lhs.isEqual(rhs)
}

let x = X(x: "x1", y: 1)
let y = X(x: "x1", y: 1)
X(x: "x1", y: 1) == X(x: "x1", y: 1) // Swift 'x == y' (true)
x.isEqual(y)                         // Obj-C '[x isEqual: y]' (true)

var s:Set<X> = [X(x: "x1", y: 1)]
s.count // count == 1
s.insert(X(x: "x2", y: 1))
s.count // count == 2
s.insert(X(x: "x1", y: 1))
s.count // count == 2
s.insert(X(x: "x2", y: 1))
s.count // count == 2

I spent time looking for the right answer to this until I hit this Question/Answer. I had reverted to basics in XCode Playground to see what was happening. Using subclasses of NSObject in Swift Set makes a bunch of more readable code.

CPD
  • 427
  • 2
  • 11
  • I don't know why so many people chose the other answer. For me, this one is the valuable and correct one. Thank you very much. – Owen Zhao Mar 29 '17 at 02:58
  • Just to mention, it is required in `NSObjectProtocol`, if you override `func isEqual(_ object: Any?) -> Bool`, you must override `var hash: Int { get }` as well. As in docs, it says `If two objects are equal (as determined by the isEqual(_:) method), they must have the same hash value. `. https://developer.apple.com/reference/objectivec/nsobjectprotocol/1418859-hash – Owen Zhao Mar 29 '17 at 03:15