Skip to content

Commit 716b58e

Browse files
committed
Handle ObjC -hash differently for Equatable and non-Equatable types
For an Equatable type, we need a hash implementation that is compatible with any possible definition of `==`. Conservatively, that means `-hash` must return a constant. For non-Equatable types, we know that `==` is identity based, so we can get better hash behavior by using the object address. Caveat: This means that non-Equatable types will do two protocol conformance checks on every call to `hash`.
1 parent 137e468 commit 716b58e

File tree

3 files changed

+44
-18
lines changed

3 files changed

+44
-18
lines changed

stdlib/public/runtime/SwiftObject.mm

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -375,20 +375,32 @@ + (BOOL)conformsToProtocol:(Protocol*)proto {
375375

376376
- (NSUInteger)hash {
377377
auto selfMetadata = _swift_getClassOfAllocated(self);
378+
379+
// If it's Hashable, use that
378380
auto hashableConformance =
379381
reinterpret_cast<const hashable_support::HashableWitnessTable *>(
380382
swift_conformsToProtocolCommon(
381383
selfMetadata, &hashable_support::HashableProtocolDescriptor));
382-
if (hashableConformance == NULL) {
383-
// If a type is Equatable (but not Hashable), we
384-
// have to return something here that is compatible
385-
// with the `isEqual:` below. NSObject's default
386-
// of `(NSUInteger)self` won't work, so we instead
387-
// return a constant fallback value:
384+
if (hashableConformance != NULL) {
385+
return _swift_stdlib_Hashable_hashValue_indirect(
386+
&self, selfMetadata, hashableConformance);
387+
}
388+
389+
// If a type is Equatable (but not Hashable), we
390+
// have to return something here that is compatible
391+
// with the `isEqual:` below. NSObject's default
392+
// of `(NSUInteger)self` won't work, so we instead
393+
// return a constant fallback value:
394+
auto equatableConformance =
395+
reinterpret_cast<const equatable_support::EquatableWitnessTable *>(
396+
swift_conformsToProtocolCommon(
397+
selfMetadata, &equatable_support::EquatableProtocolDescriptor));
398+
if (equatableConformance != nullptr) {
388399
return (NSUInteger)1;
389400
}
390-
return _swift_stdlib_Hashable_hashValue_indirect(
391-
&self, selfMetadata, hashableConformance);
401+
402+
// Legacy default for types that are neither Hashable nor Equatable.
403+
return (NSUInteger)self;
392404
}
393405

394406
- (BOOL)isEqual:(id)other {

test/stdlib/Inputs/SwiftObjectNSObject/SwiftObjectNSObject.m

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,12 @@ void TestSwiftObjectNSObjectHashValue(id e, NSUInteger hashValue)
116116
expectTrue([e hash] == hashValue);
117117
}
118118

119+
void TestSwiftObjectNSObjectDefaultHashValue(id e)
120+
{
121+
NSUInteger hashValue = (NSUInteger)e;
122+
TestSwiftObjectNSObjectHashValue(e, hashValue);
123+
}
124+
119125
void TestSwiftObjectNSObject(id c, id d)
120126
{
121127
printf("TestSwiftObjectNSObject\n");

test/stdlib/SwiftObjectNSObject.swift

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ func TestSwiftObjectNSObjectEquals(_: AnyObject, _: AnyObject)
7979
func TestSwiftObjectNSObjectNotEquals(_: AnyObject, _: AnyObject)
8080
@_silgen_name("TestSwiftObjectNSObjectHashValue")
8181
func TestSwiftObjectNSObjectHashValue(_: AnyObject, _: Int)
82+
@_silgen_name("TestSwiftObjectNSObjectDefaultHashValue")
83+
func TestSwiftObjectNSObjectDefaultHashValue(_: AnyObject)
8284
@_silgen_name("TestSwiftObjectNSObjectAssertNoErrors")
8385
func TestSwiftObjectNSObjectAssertNoErrors()
8486
@@ -101,14 +103,18 @@ func TestHashable(_ h: H)
101103
TestSwiftObjectNSObjectHashValue(h, h.hashValue)
102104
}
103105
104-
// Test Obj-C hashValue for Swift types that are not Hashable
105-
func TestNonHashableHash(_ e: AnyObject)
106+
// Test Obj-C hashValue for Swift types that are Equatable but not Hashable
107+
func TestEquatableHash(_ e: AnyObject)
106108
{
107-
// Non-Hashable type should always have the
108-
// same hash value in Obj-C
109+
// These should have a constant hash value
109110
TestSwiftObjectNSObjectHashValue(e, 1)
110111
}
111112
113+
func TestNonEquatableHash(_ e: AnyObject)
114+
{
115+
TestSwiftObjectNSObjectDefaultHashValue(e)
116+
}
117+
112118
// This check is for NSLog() output from TestSwiftObjectNSObject().
113119
// CHECK: c ##SwiftObjectNSObject.C##
114120
// CHECK-NEXT: d ##SwiftObjectNSObject.D##
@@ -154,12 +160,14 @@ if #available(OSX 10.12, iOS 10.0, *) {
154160
TestNonEquatableEquals(F1(i: 1), E(i: 1))
155161
TestEquatableEquals(H(i:1), E(i:1))
156162
157-
// Non-Hashable: alway have the same Obj-C hashValue
158-
TestNonHashableHash(E(i: 1))
159-
TestNonHashableHash(E1(i: 3))
160-
TestNonHashableHash(E2(i: 8))
161-
TestNonHashableHash(C())
162-
TestNonHashableHash(D())
163+
// Equatable but not Hashable: alway have the same Obj-C hashValue
164+
TestEquatableHash(E(i: 1))
165+
TestEquatableHash(E1(i: 3))
166+
TestEquatableHash(E2(i: 8))
167+
168+
// Neither Equatable nor Hashable
169+
TestNonEquatableHash(C())
170+
TestNonEquatableHash(D())
163171
164172
// Hashable types are also Equatable
165173
TestEquatableEquals(H(i:1), H(i:1))

0 commit comments

Comments
 (0)