When subclassing NSObject in Swift, should you override hash or implement Hashable?
Also, should you override isEqual: or implement the == operator?
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.
For NSObject it is best to override hash and isEqual. It already conforms to Hashable and Equatable and has synthesized conformances for that which in turn invoke hash and isEqual. So since it is an NSObject, do it the ObjC way and override the values that also affect the ObjC hash value and equality.
class Identity: NSObject {
let name: String
let email: String
init(name: String, email: String) {
self.name = name
self.email = email
}
override var hash: Int {
var hasher = Hasher()
hasher.combine(name)
hasher.combine(email)
return hasher.finalize()
}
override func isEqual(_ object: Any?) -> Bool {
guard let other = object as? Identity else {
return false
}
return name == other.name && email == other.email
}
}
"Also, should you override isEqual: or implement ==?"
You could do both. And in case you'd make the implementations
behave differently you'd add flare to the life of the users of your code too. Been there, done that. It's fun.
Implement Hashable, which also requires you to implement the == operator for your type. These are used for a lot of useful stuff in the Swift standard library like the indexOf function which only works on collections of a type that implements Equatable, or the Set<T> type which only works with types that implement Hashable.
Related
I've looked around, and haven't seen an answer to my question (but maybe I should be able to infer one).
I have an object, based on a protocol. It's an associated type protocol, with class operators that are defined, based on the type assigned to associatedtype, like so:
protocol GenericBaseProtocol {
associatedtype T
var myProperty: T {get set}
init(_ myProperty: T )
}
extension GenericBaseProtocol where T: Equatable {
static func ==(lhs: Self, rhs: Self) -> Bool {
return lhs.myProperty == rhs.myProperty
}
}
So if I create a class, based on this, and give T an Equatable type, like so:
class IntClass: GenericBaseProtocol {
typealias T = Int
var myProperty: T = 0
required init(_ myProperty: T ) {
self.myProperty = myProperty
}
}
The resulting object should be comparable, like so:
let lhs = IntClass(3)
let rhs = IntClass(4)
let isEqual = lhs == rhs
Cool. Now, if I then create an instance with a non-Equatable type, like so:
class ArrayClass: GenericBaseProtocol {
typealias T = [String]
var myProperty: T = []
required init(_ myProperty: T ) {
self.myProperty = myProperty
}
}
And instantiate that, like so:
let lhs2A = ArrayClass(["HI"])
let rhs2A = ArrayClass(["Howaya"])
I will have compile-time syntax errors when I try this:
let isEqual = lhs2A == rhs2A
What I'd like to be able to do, is test the class object of lhs2A, and see if it implements static func ==(lhs: Self, rhs: Self) -> Bool
I'm not sure this can be done, but it would be nice for this article I'm writing up if I could add a runtime/guard proof to the playground, instead of simply commenting out the code.
Any ideas?
You could extend your protocol to give it a default implementation for the == operator in cases where the associated type is not Equatable.
This could also be used to provide a runtime indicator of wether the type is equatable or not.
for example:
extension GenericBaseProtocol where T: Equatable {
static func ==(lhs: Self, rhs: Self) -> Bool {
return lhs.myProperty == rhs.myProperty
}
var isEquatable:Bool { return true }
}
extension GenericBaseProtocol {
static func ==(lhs: Self, rhs: Self) -> Bool {
return false
}
var isEquatable:Bool { return false }
}
When subclassing NSObject in Swift, should you override hash or implement Hashable?
Also, should you override isEqual: or implement the == operator?
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.
For NSObject it is best to override hash and isEqual. It already conforms to Hashable and Equatable and has synthesized conformances for that which in turn invoke hash and isEqual. So since it is an NSObject, do it the ObjC way and override the values that also affect the ObjC hash value and equality.
class Identity: NSObject {
let name: String
let email: String
init(name: String, email: String) {
self.name = name
self.email = email
}
override var hash: Int {
var hasher = Hasher()
hasher.combine(name)
hasher.combine(email)
return hasher.finalize()
}
override func isEqual(_ object: Any?) -> Bool {
guard let other = object as? Identity else {
return false
}
return name == other.name && email == other.email
}
}
"Also, should you override isEqual: or implement ==?"
You could do both. And in case you'd make the implementations
behave differently you'd add flare to the life of the users of your code too. Been there, done that. It's fun.
Implement Hashable, which also requires you to implement the == operator for your type. These are used for a lot of useful stuff in the Swift standard library like the indexOf function which only works on collections of a type that implements Equatable, or the Set<T> type which only works with types that implement Hashable.
Why do I have to add != to make the comparison correct?
import UIKit
class Person: NSObject {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
extension Person {
static func ==(lhs: Person, rhs: Person) -> Bool {
return lhs.name == rhs.name && lhs.age == rhs.age
}
static func !=(lhs: Person, rhs: Person) -> Bool {
return !(lhs == rhs)
}
}
let first = Person(name: "John", age: 26)
let second = Person(name: "John", age: 26)
/**
* return false (which is correct) when we implement != function. But,
* it will return true if we don't implement the != function.
*/
first != second
Update:
So I got why I had to add != function to make it work. it's because the class inherit the NSObject which uses isEqual method behind the scene. But why does adding != function make it work? Any explanation here?
NSObject conforms to Equatable but uses its own isEqual method and in terms of isEqual both instances are not equal. NSObject calls == only if your form of != is implemented, which contains ==.
If you delete NSObject (and add Equatable) the implementation of == works as expected.
The recommended way for NSObject is to override isEqual with a custom implementation and omit == (and !=).
Sorry, this is not a direct answer to your question.
As Alexander commented, Swift Standard Library has this default implementation of !=:
Equatable.swift
#_transparent
public static func != (lhs: Self, rhs: Self) -> Bool {
return !(lhs == rhs)
}
I cannot explain this behavior well, but the == operator in the default implementation above is solved to the default == operator for NSObject, as NSObject (and also its descendants) is already Equatable and has an == operator to conform to Equatable. So, even if the explicit representation is exactly the same as your != definition, the == operators are solved to different implementations.
A general guidline to define your own equality to an NSObject-descendant class:
Make == and isEqual(_:) consistent
You may store your class's instance inside NSArray or NSDictionary (in many cases implicitly). Inside their methods, isEqual(_:) is used when equality check is needed, not the == operator.
So, just defining the == operator without giving a consistent override to isEqual(_:), such methods will generate unexpected result.
To make consistent == and isEqual(_:),
just override only isEqual(_:) and do not define == and != explicitly.
The default implementation of == for NSObject (and also !=) uses isEqual(_:).
class Person: NSObject {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
override func isEqual(_ object: Any?) -> Bool {
if let other = object as? Person {
return self.name == other.name && self.age == other.age
}
return false
}
}
(See ONE MORE THING at the bottom.)
ADDITION
Similar behavior can be found on non-NSObject classes.
class BaseClass {
var a: Int
init(a: Int) {
self.a = a
}
}
extension BaseClass: Equatable {
static func == (lhs: BaseClass, rhs: BaseClass) -> Bool {
print("`==` of BaseClass")
return lhs.a == rhs.a
}
}
let b1 = BaseClass(a: 0)
let b2 = BaseClass(a: 0)
print(b1 != b2) //->`==` of BaseClass, false ### as expected
class DerivedClass: BaseClass {
var b: Int
init(a: Int, b: Int) {
self.b = b
super.init(a: a)
}
}
extension DerivedClass {
static func == (lhs: DerivedClass, rhs: DerivedClass) -> Bool {
print("`==` of DerivedClass")
return lhs.a == rhs.a && lhs.b == rhs.b
}
}
let d1 = DerivedClass(a: 0, b: 1)
let d2 = DerivedClass(a: 0, b: 2)
print(d1 != d2) //->`==` of BaseClass, false ### `==` of DerivedClass and true expected
Seems we need extra care when overriding == for already Equatable classes.
ONE MORE THING
(Thanks for Hamish.)
You know you need to implement == and hashValue consistently when creating a type conforming to Hashable. NSObject is declared as Hashable, and its hashValue needs to be consistent with hash. So, when you override isEqual(_:) in your NSObject-descendent, you also should override hash consistent with your overridden isEqual(_:).
So, your Person class should be something like this:
class Person: NSObject {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
override func isEqual(_ object: Any?) -> Bool {
if let other = object as? Person {
return self.name == other.name && self.age == other.age
}
return false
}
override var hash: Int {
//### This is just an example, but not too bad in practical use cases.
return name.hashValue ^ age.hashValue
}
}
What you are doing is wrong. You should not implement == or !=. An NSObject subclass automatically implements == as isEqual:. You are disrupting that. You should implement isEqual: and that's all.
I am getting frustrated with how Swift handles equality. Or I'm just missing something. How come these 2 indexOf DOESN'T work the same way?
let first = self.objects.indexOf(object) //This returns nil
let second = self.objects.indexOf{$0 == object} //This returns the index
My == override:
func ==(lhs: MyObject, rhs: MyObject) -> Bool {
return lhs.someProperty == rhs.someProperty
}
The == override doesn't get called in the first indexOf. Why is that so? This feels really dangerous.
(MyObject is a subclass of PFObject (Parse.com objects). I don't know if this is what's messing this up.)
Since your Object is subclass of PFObject, and PFObject is subclass of NSObject, and NSObject already confirm to Equatable by using the isEqual method to implement. So your == operator override is not working. You should override the isEqual method. (To be honest, it's horrible :(
sample code:
class Object: NSObject {
var value: Int
init(value: Int) {
self.value = value
}
override func isEqual(object: AnyObject?) -> Bool {
guard let obj = object as? Object else { return false }
return self.value == obj.value
}
}
// If your class is not inherit from any ObjC class:
//extension Object: Equatable {}
//func ==(lhs: Object, rhs: Object) -> Bool {
// return lhs.value == rhs.value
//}
let c = Object(value: 3)
let objs = [Object(value: 1), Object(value: 2), Object(value: 3)]
let index = objs.indexOf(c)
let index2 = objs.indexOf { $0 == c }
print(index) // Optional(2)
print(index2) // Optional(2)
The second indexOf method that you are using allows you to define a closure to indicate object equality (like you have already done). The first one according to the documentation:
Returns the first index where value appears in self or nil if value is not found.
Where value is the object you are passing as the parameter. Without seeing the implementation of this version I can only assume they compare if the two objects are equal in every way (all properties match). So in your case, if you only consider the objects equal if the somePropertys match, then you should be using the closure method with your overloaded operator.
static func == (left: MyClass, right: MyClass) -> Bool {
return left.attribute1 == right.attribute1
}
override func isEqual(_ object: Any?) -> Bool {
guard let right = object as? MyClass else {
return false
}
// call the == from above
// why couldn't apple do this ???
//
return self == right
}
Let's say we have a protocol in Swift:
#objc protocol FancyViewDelegate {
optional func fancyView(view: FancyView, didSelectSegmentAtIndex index: Int)
optional func fancyView(view: FancyView, shouldHighlightSegmentAtIndex index: Int) -> Bool
}
Note that both methods are optional and have the same prefix signature.
Now our FancyView class looks like this:
class FancyView: UIView {
var delegate: FancyViewDelegate?
private func somethingHappened() {
guard let delegateImpl = delegate?.fancyView else {
return
}
let idx = doALotOfWorkToFindTheIndex()
delegateImpl(self, idx)
}
}
The compiler jumps in our face:
We could change somethingHappened() to this:
private func somethingHappened() {
let idx = doALotOfWorkToFindTheIndex()
delegate?.fancyView?(self, didSelectSegmentAtIndex: idx)
}
However, as you can see we risk doing a lot of work only to throw away the index afterwards, because the delegate does not implement the optional method.
The question is: How do we if let or guard let bind the implementation of two optional methods with a similar prefix signature.
First, your objective C protocol needs to confirm to NSObjectProtocol to ensure we can introspect if it supports a given method.
Then when we want to call specific method, check if that method is supported by conforming object and if yes, then perform necessary computations needed to call that method. I tried this code for instance-
#objc protocol FancyViewDelegate : NSObjectProtocol {
optional func fancyView(view: UIView, didSelectSegmentAtIndex index: Int)
optional func fancyView(view: UIView, shouldHighlightSegmentAtIndex index: Int) -> Bool
}
class FancyView: UIView {
var delegate: FancyViewDelegate?
private func somethingHappened() {
if delegate?.respondsToSelector("fancyView:didSelectSegmentAtIndex") == true {
let idx :Int = 0 //Compute the index here
delegate?.fancyView!(self, didSelectSegmentAtIndex: idx)
}
}
}