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 theisEqual:
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 theisEqual:
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 theNSObject
class is equivalent to an identity check by pointer equality. You can overrideisEqual:
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 theNSObject
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 ofisEqual:
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
How to Deal With @Objc Inference Deprecation With #Selector() in Swift 4
Do Swift-Based Applications Work on Os X 10.9/Ios 7 and Lower
Load a Uiview from Nib in Swift
Cfrunloop in Swift Command Line Program
How to Detect If an Skspritenode Has Been Touched
Detect When a Tab Bar Item Is Pressed
Extend Array Types Using Where Clause in Swift
How to Atomically Increment a Variable in Swift
Firebase Getting Data in Order
How to Use String.Substringwithrange? (Or, How Do Ranges Work in Swift)
Why Optional Constant Does Not Automatically Have a Default Value of Nil
Get HTML from Wkwebview in Swift
How to Unwrap Double Optionals
Transparent Background For Modally Presented Viewcontroller
Swift - What's the Difference Between Metatype .Type and .Self