Skip to content

Commit 13bc567

Browse files
committed
[Foundation] Notification: Add note on == not being reflexive and stabilize hashing
The `ObjectIdentifier(object as AnyObject)` is not necessarily stable; this breaks reflexivity for ==, and it makes the hash encoding nondeterministic.
1 parent 582b65b commit 13bc567

File tree

2 files changed

+70
-6
lines changed

2 files changed

+70
-6
lines changed

stdlib/public/Darwin/Foundation/Notification.swift

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,17 @@ public struct Notification : ReferenceConvertible, Equatable, Hashable {
4141

4242
public func hash(into hasher: inout Hasher) {
4343
hasher.combine(name)
44-
hasher.combine(ObjectIdentifier(object as AnyObject))
45-
// `userInfo` is not directly hashable, but == compares it by bridging
46-
// it to NSDictionary, so its contents should be hashed here. However,
47-
// to prevent potential issues with values fail to correctly implement
48-
// `NSObject.hash`, we only hash the keys here.
44+
45+
// FIXME: We should feed `object` to the hasher, too. However, this
46+
// cannot be safely done if its value happens not to be Hashable.
47+
48+
// FIXME: == compares `userInfo` by bridging it to NSDictionary, so its
49+
// contents should be fully hashed here. However, to prevent stability
50+
// issues with userInfo dictionaries that include non-Hashable values,
51+
// we only feed the keys to the hasher here.
4952
//
50-
// FIXME: This makes for weak hashes. We really should hash everything.
53+
// FIXME: This makes for weak hashes. We really should hash everything
54+
// that's compared in ==.
5155
//
5256
// The algorithm below is the same as the one used by Dictionary.
5357
var commutativeKeyHash = 0
@@ -79,6 +83,10 @@ public struct Notification : ReferenceConvertible, Equatable, Hashable {
7983
}
8084
if let lhsObj = lhs.object {
8185
if let rhsObj = rhs.object {
86+
// FIXME: Converting an arbitrary value to AnyObject won't use a
87+
// stable address when the value needs to be boxed. Therefore,
88+
// this comparison makes == non-reflexive, violating Equatable
89+
// requirements. (rdar://problem/49797185)
8290
if lhsObj as AnyObject !== rhsObj as AnyObject {
8391
return false
8492
}
@@ -91,6 +99,9 @@ public struct Notification : ReferenceConvertible, Equatable, Hashable {
9199
if lhs.userInfo != nil {
92100
if rhs.userInfo != nil {
93101
// user info must be compared in the object form since the userInfo in swift is not comparable
102+
103+
// FIXME: This violates reflexivity when userInfo contains any
104+
// non-Hashable values, for the same reason as described above.
94105
return lhs._bridgeToObjectiveC() == rhs._bridgeToObjectiveC()
95106
} else {
96107
return false

test/stdlib/TestNotification.swift

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,64 @@ class TestNotification : TestNotificationSuper {
3232
func test_unconditionallyBridgeFromObjectiveC() {
3333
expectEqual(Notification(name: Notification.Name("")), Notification._unconditionallyBridgeFromObjectiveC(nil))
3434
}
35+
36+
func test_hashing() {
37+
let o1 = NSObject()
38+
let o2 = NSObject()
39+
let values: [Notification] = [
40+
/* 0 */ Notification(name: .init("a"), object: o1, userInfo: nil),
41+
/* 1 */ Notification(name: .init("b"), object: o1, userInfo: nil),
42+
/* 2 */ Notification(name: .init("a"), object: o2, userInfo: nil),
43+
/* 3 */ Notification(name: .init("a"), object: o1, userInfo: ["Foo": 1]),
44+
/* 4 */ Notification(name: .init("a"), object: o1, userInfo: ["Foo": 2]),
45+
/* 5 */ Notification(name: .init("a"), object: o1, userInfo: ["Bar": 1]),
46+
/* 6 */ Notification(name: .init("a"), object: o1, userInfo: ["Foo": 1, "Bar": 2]),
47+
]
48+
49+
let hashException: Set<Int> = [3, 4]
50+
51+
checkHashable(
52+
values,
53+
equalityOracle: { $0 == $1 },
54+
hashEqualityOracle: {
55+
// FIXME: Unfortunately cases 3 and 4 above currently hash the
56+
// same way, even though they compare different.
57+
$0 == $1 || (hashException.contains($0) && hashException.contains($1))
58+
})
59+
}
3560
}
3661

3762

3863
#if !FOUNDATION_XCTEST
3964
var NotificationTests = TestSuite("TestNotification")
4065
NotificationTests.test("test_unconditionallyBridgeFromObjectiveC") { TestNotification().test_unconditionallyBridgeFromObjectiveC() }
66+
NotificationTests.test("test_hashing") { TestNotification().test_hashing() }
67+
68+
private struct NonHashableValueType: Equatable {
69+
let value: Int
70+
init(_ value: Int) {
71+
self.value = value
72+
}
73+
}
74+
75+
NotificationTests.test("test_reflexivity_violation")
76+
.xfail(
77+
.custom({ true },
78+
reason: "<rdar://problem/49797185> Foundation.Notification's equality relation isn't reflexive"))
79+
.code {
80+
let name = Notification.Name("name")
81+
let a = NonHashableValueType(1)
82+
let b = NonHashableValueType(2)
83+
// Currently none of these values compare equal to themselves:
84+
let values: [Notification] = [
85+
Notification(name: name, object: a, userInfo: nil),
86+
Notification(name: name, object: b, userInfo: nil),
87+
Notification(name: name, object: nil, userInfo: ["foo": a]),
88+
Notification(name: name, object: nil, userInfo: ["foo": b]),
89+
]
90+
checkHashable(values)
91+
}
92+
93+
4194
runAllTests()
4295
#endif

0 commit comments

Comments
 (0)