Skip to content

Commit 8bd5650

Browse files
committed
[stdlib] Add hashing methods to RawRepresentable to match ==
1 parent 5df5711 commit 8bd5650

File tree

2 files changed

+77
-0
lines changed

2 files changed

+77
-0
lines changed

stdlib/public/core/CompilerProtocols.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,27 @@ public func != <T : Equatable>(lhs: T, rhs: T) -> Bool
178178
return lhs.rawValue != rhs.rawValue
179179
}
180180

181+
// Ensure that any RawRepresentable types that conform to Hashable without
182+
// providing explicit implementations get hashing that's consistent with the ==
183+
// definition above. (Compiler-synthesized hashing is based on stored properties
184+
// rather than rawValue; the difference is subtle, but it can be fatal.)
185+
extension RawRepresentable where RawValue: Hashable, Self: Hashable {
186+
@inlinable // trivial
187+
public var hashValue: Int {
188+
return rawValue.hashValue
189+
}
190+
191+
@inlinable // trivial
192+
public func hash(into hasher: inout Hasher) {
193+
hasher.combine(rawValue)
194+
}
195+
196+
@inlinable // trivial
197+
public func _rawHashValue(seed: Int) -> Int {
198+
return rawValue._rawHashValue(seed: seed)
199+
}
200+
}
201+
181202
/// A type that provides a collection of all of its values.
182203
///
183204
/// Types that conform to the `CaseIterable` protocol are typically
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// RUN: %target-run-simple-swift | %FileCheck %s
2+
// REQUIRES: executable_test
3+
4+
// RawRepresentable is not Equatable itself, but it does provide a generic
5+
// implementation of == based on rawValues. This gets picked up as the
6+
// implementation of Equatable.== when a concrete RawRepresentable type conforms
7+
// to Equatable without providing its own implementation.
8+
//
9+
// However, RawRepresentable used to not provide equivalent implementations for
10+
// hashing, allowing the compiler to synthesized hashing as usual, based on the
11+
// actual contents of the type rather than its rawValue. Thus, the definitions
12+
// of equality and hashing may not actually match, leading to broken hashes.
13+
//
14+
// The difference between rawValue and the actual contents is subtle, and it
15+
// only causes problems in custom RawRepresentable implementations where the
16+
// rawValue isn't actually the storage representation, like the weird struct
17+
// below.
18+
//
19+
// rdar://problem/45308741
20+
21+
struct TrickyRawRepresentable: RawRepresentable, Hashable {
22+
var value: [Unicode.Scalar]
23+
24+
var rawValue: String {
25+
return String(String.UnicodeScalarView(value))
26+
}
27+
28+
init?(rawValue: String) {
29+
self.value = Array(rawValue.unicodeScalars)
30+
}
31+
}
32+
33+
let s1 = TrickyRawRepresentable(rawValue: "café")!
34+
let s2 = TrickyRawRepresentable(rawValue: "cafe\u{301}")!
35+
36+
// CHECK: s1 == s2: true
37+
print("s1 == s2: \(s1 == s2)")
38+
39+
// CHECK: hashValue matches: true
40+
print("hashValue matches: \(s1.hashValue == s2.hashValue)")
41+
42+
extension Hasher {
43+
static func hash<H: Hashable>(_ value: H) -> Int {
44+
var hasher = Hasher()
45+
hasher.combine(value)
46+
return hasher.finalize()
47+
}
48+
}
49+
50+
// CHECK: hash(into:) matches: true
51+
print("hash(into:) matches: \(Hasher.hash(s1) == Hasher.hash(s2))")
52+
53+
// CHECK: _rawHashValue(seed:) matches: true
54+
let r1 = s1._rawHashValue(seed: 42)
55+
let r2 = s2._rawHashValue(seed: 42)
56+
print("_rawHashValue(seed:) matches: \(r1 == r2)")

0 commit comments

Comments
 (0)