Nsobject Subclass in Swift: Hash VS Hashvalue, Isequal VS ==

NSObject subclass in Swift: hash vs hashValue, isEqual vs ==

NSObject already conforms to the Hashable protocol:

extension NSObject : Equatable, Hashable {
/// The hash value.
///
/// **Axiom:** `x == y` implies `x.hashValue == y.hashValue`
///
/// - Note: the hash value is not guaranteed to be stable across
/// different invocations of the same program. Do not persist the
/// hash value across program runs.
public var hashValue: Int { get }
}

public func ==(lhs: NSObject, rhs: NSObject) -> Bool

I could not find an official reference, but it seems that hashValue
calls the hash method from NSObjectProtocol, and == calls the
isEqual: method (from the same protocol). See update at the
end of the answer!

For NSObject subclasses, the correct way seems to be
to override hash and isEqual:, and here is an experiment which
demonstrates that:

1. Override hashValue and ==

class ClassA : NSObject {
let value : Int

init(value : Int) {
self.value = value
super.init()
}

override var hashValue : Int {
return value
}
}

func ==(lhs: ClassA, rhs: ClassA) -> Bool {
return lhs.value == rhs.value
}

Now create two different instances of the class which are considered
"equal" and put them into a set:

let a1 = ClassA(value: 13)
let a2 = ClassA(value: 13)

let nsSetA = NSSet(objects: a1, a2)
let swSetA = Set([a1, a2])

print(nsSetA.count) // 2
print(swSetA.count) // 2

As you can see, both NSSet and Set treat the objects as different.
This is not the desired result. Arrays have unexpected results as well:

let nsArrayA = NSArray(object: a1)
let swArrayA = [a1]

print(nsArrayA.indexOfObject(a2)) // 9223372036854775807 == NSNotFound
print(swArrayA.indexOf(a2)) // nil

Setting breakpoints or adding debug output reveals that the overridden
== operator is never called. I don't know if this is a bug or
intended behavior.

2. Override hash and isEqual:

class ClassB : NSObject {
let value : Int

init(value : Int) {
self.value = value
super.init()
}

override var hash : Int {
return value
}

override func isEqual(object: AnyObject?) -> Bool {
if let other = object as? ClassB {
return self.value == other.value
} else {
return false
}
}
}

For Swift 3, the definition of isEqual: changed to

override func isEqual(_ object: Any?) -> Bool { ... }

Now all results are as expected:

let b1 = ClassB(value: 13)
let b2 = ClassB(value: 13)

let nsSetB = NSSet(objects: b1, b2)
let swSetB = Set([b1, b2])

print(swSetB.count) // 1
print(nsSetB.count) // 1

let nsArrayB = NSArray(object: b1)
let swArrayB = [b1]

print(nsArrayB.indexOfObject(b2)) // 0
print(swArrayB.indexOf(b2)) // Optional(0)

Update: The behavior is documented in the book "Using Swift with Cocoa and Objective-C", under "Interacting with Objective-C API":

The default implementation of the == operator invokes the isEqual: method, and the default implementation of the === operator checks pointer equality. You should not override the equality or identity operators for types imported from Objective-C.

The base implementation of the isEqual: provided by the NSObject class is equivalent to an identity check by pointer equality. You can override isEqual: in a subclass to have Swift and Objective-C APIs determine equality based on the contents of objects rather than their identities.

The book is available in the Apple Book app.

It was also documented on Apple's website but was removed, and is still visible on the WebArchive snapshot of the page.

Difference between Swift's hash and hashValue

hash is a required property in the NSObject protocol, which groups methods that are fundamental to all Objective-C objects, so that predates Swift.
The default implementation just returns the objects address,
as one can see in
NSObject.mm, but one can override the property
in NSObject subclasses.

hashValue is a required property of the Swift Hashable protocol.

Both are connected via a NSObject extension defined in the
Swift standard library in
ObjectiveC.swift:

extension NSObject : Equatable, Hashable {
/// The hash value.
///
/// **Axiom:** `x == y` implies `x.hashValue == y.hashValue`
///
/// - Note: the hash value is not guaranteed to be stable across
/// different invocations of the same program. Do not persist the
/// hash value across program runs.
open var hashValue: Int {
return hash
}
}

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

(For the meaning of open var, see What is the 'open' keyword in Swift?.)

So NSObject (and all subclasses) conform to the Hashable
protocol, and the default hashValue implementation
return the hash property of the object.

A similar relationship exists between the isEqual method of the
NSObject protocol, and the == operator from the Equatable
protocol: NSObject (and all subclasses) conform to the Equatable
protocol, and the default == implementation
calls the isEqual: method on the operands.

What is the NSObject isEqual: and hash default function?

As you've correctly guessed, NSObject's default isEqual: behaviour is comparing the memory address of the object. Strangely, this is not presently documented in the NSObject Class Reference, but it is documented in the Introspection documentation, which states:

The default NSObject implementation of isEqual: simply checks for pointer equality.

Of course, as you are doubtless aware, subclasses of NSObject can override isEqual: to behave differently. For example, NSString's isEqual: method, when passed another NSString, will first check the address and then check for an exact literal match between the strings.

Swift 2.0 Set not working as expected when containing NSObject subclass

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
}
}

Combining hash of Date and String

For NSObject subclasses you must override the hash property and the isEqual: method, compare NSObject subclass in Swift: hash vs hashValue, isEqual vs ==.

For the implementation of the hash property you can use the Hasher class introduced in Swift 4.2, and its combine() method:

Adds the given value to this hasher, mixing its essential parts into the hasher state.

I would also suggest to make the properties constant, since mutating them after an object is inserted into a hashable collection (set, dictionary) causes undefined behaviour, compare Swift mutable set: Duplicate element found.

class Model: NSObject { /* ... */ }


class Something : Model {
let name: String
let time: Date

init(name: String, time: Date) {
self.name = name
self.time = time
}

override var hash : Int {
var hasher = Hasher()
hasher.combine(name)
hasher.combine(time)
return hasher.finalize()
}

static func ==(lhs: Something, rhs: Something) -> Bool {
return lhs.name == rhs.name && lhs.time == rhs.time
}
}


Related Topics



Leave a reply



Submit