mirror of
https://github.com/apple/swift.git
synced 2025-12-14 20:36:38 +01:00
AnyHashable has numerous edge cases where two AnyHashable values compare equal but produce different hashes. This breaks Set and Dictionary invariants and can cause unexpected behavior and/or traps. This change overhauls AnyHashable's implementation to fix these edge cases, hopefully without introducing new issues.
- Fix transitivity of ==. Previously, comparisons involving AnyHashable values with Objective-C provenance were handled specially, breaking Equatable:
let a = (42 as Int as AnyHashable)
let b = (42 as NSNumber as AnyHashable)
let c = (42 as Double as AnyHashable)
a == b // true
b == c // true
a == c // was false(!), now true
let d = ("foo" as AnyHashable)
let e = ("foo" as NSString as AnyHashable)
let f = ("foo" as NSString as NSAttributedStringKey as AnyHashable)
d == e // true
e == f // true
d == f // was false(!), now true
- Fix Hashable conformance for numeric types boxed into AnyHashable:
b == c // true
b.hashValue == c.hashValue // was false(!), now true
Fixing this required adding a custom AnyHashable box for all standard integer and floating point types. The custom box was needed to ensure that two AnyHashables containing the same number compare equal and hash the same way, no matter what their original type was. (This behavior is required to ensure consistency with NSNumber, which has not been preserving types since SE-0170.
- Add custom AnyHashable representations for Arrays, Sets and Dictionaries, so that when they contain numeric types, they hash correctly under the new rules above.
- Remove AnyHashable._usedCustomRepresentation. The provenance of a value should not affect its behavior.
- Allow AnyHashable values to be downcasted into compatible types more often.
- Forward _rawHashValue(seed:) to AnyHashable box. This fixes AnyHashable hashing for types that customize single-shot hashing.
https://bugs.swift.org/browse/SR-7496
rdar://problem/39648819
252 lines
7.5 KiB
Plaintext
252 lines
7.5 KiB
Plaintext
// RUN: %empty-directory(%t)
|
|
//
|
|
// RUN: %gyb %s -o %t/AnyHashableCasts.swift
|
|
// RUN: %line-directive %t/AnyHashableCasts.swift -- %target-build-swift -g -module-name a %t/AnyHashableCasts.swift -o %t.out
|
|
// RUN: %line-directive %t/AnyHashableCasts.swift -- %target-run %t.out
|
|
// RUN: %line-directive %t/AnyHashableCasts.swift -- %target-build-swift -g -O -module-name a %t/AnyHashableCasts.swift -o %t.out.optimized
|
|
// RUN: %line-directive %t/AnyHashableCasts.swift -- %target-run %t.out.optimized
|
|
// REQUIRES: executable_test
|
|
|
|
import StdlibUnittest
|
|
|
|
#if _runtime(_ObjC)
|
|
import Foundation
|
|
#endif
|
|
|
|
var AnyHashableCasts = TestSuite("AnyHashableCasts")
|
|
|
|
protocol Implemented {}
|
|
protocol Unimplemented {}
|
|
extension Int : Implemented {}
|
|
|
|
struct HashableStruct : Hashable {
|
|
var value : Int
|
|
static func ==(lhs: HashableStruct, rhs: HashableStruct) -> Bool {
|
|
return lhs.value == rhs.value
|
|
}
|
|
var hashValue : Int { return value }
|
|
}
|
|
|
|
class HashableClass : Hashable {
|
|
var value : Int
|
|
init(value v: Int) { self.value = v }
|
|
static func ==(lhs: HashableClass, rhs: HashableClass) -> Bool {
|
|
return lhs.value == rhs.value
|
|
}
|
|
var hashValue : Int { return value }
|
|
}
|
|
|
|
enum HashableEnum : Hashable {
|
|
case value(Int)
|
|
static func ==(lhs: HashableEnum, rhs: HashableEnum) -> Bool {
|
|
switch (lhs, rhs) {
|
|
case (.value(let l), .value(let r)): return l == r
|
|
}
|
|
}
|
|
var hashValue : Int {
|
|
switch self {
|
|
case .value(let v): return v
|
|
}
|
|
}
|
|
}
|
|
|
|
func opaqueCast<T, U>(_ lhs: T, _ rhs: U.Type) -> U? {
|
|
return lhs as? U
|
|
}
|
|
|
|
func matches<T: Equatable>(_ lhs: T, _ rhs: T) -> Bool {
|
|
return lhs == rhs
|
|
}
|
|
|
|
func matches<T: Equatable, U>(_ lhs: U, _ rhs: T) -> Bool {
|
|
if let x = lhs as? T {
|
|
return x == rhs
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
%{
|
|
testCases = [
|
|
("5", "AnyHashable", "Int", "5"),
|
|
("5", "AnyHashable", "Any", "5"),
|
|
("5", "AnyHashable", "Implemented", "5"),
|
|
("5", "AnyHashable", "Unimplemented", False),
|
|
("5", "Int", "AnyHashable", "5"),
|
|
("5", "Any", "AnyHashable", "5"),
|
|
("AnyHashable(5)", "Any", "Int", "5"),
|
|
("HashableStruct(value: 5)", "HashableStruct", "AnyHashable",
|
|
"AnyHashable(HashableStruct(value: 5))"),
|
|
("HashableClass(value: 5)", "HashableClass", "AnyHashable",
|
|
"AnyHashable(HashableClass(value: 5))"),
|
|
("HashableEnum.value(5)", "HashableEnum", "AnyHashable",
|
|
"AnyHashable(HashableEnum.value(5))"),
|
|
]
|
|
}%
|
|
|
|
% for valueExpr, coercedType, castType, expected in testCases:
|
|
AnyHashableCasts.test("${valueExpr} as ${coercedType} as? ${castType}") {
|
|
do {
|
|
let x = ${valueExpr}
|
|
let y : ${coercedType} = x
|
|
if let z = y as? ${castType} {
|
|
%if expected:
|
|
expectTrue(matches(z, ${expected}))
|
|
%else:
|
|
expectUnreachable()
|
|
%end
|
|
} else {
|
|
%if expected:
|
|
expectUnreachable()
|
|
%end
|
|
}
|
|
|
|
if let z = opaqueCast(y, (${castType}).self) {
|
|
%if expected:
|
|
expectTrue(matches(z, ${expected}))
|
|
%else:
|
|
expectUnreachable()
|
|
%end
|
|
} else {
|
|
%if expected:
|
|
expectUnreachable()
|
|
%end
|
|
}
|
|
}
|
|
}
|
|
% end
|
|
|
|
#if _runtime(_ObjC)
|
|
// A wrapper type around Int that bridges to NSNumber.
|
|
struct IntWrapper1: _SwiftNewtypeWrapper, Hashable, _ObjectiveCBridgeable {
|
|
let rawValue: Int
|
|
}
|
|
|
|
// A wrapper type around Int that bridges to NSNumber.
|
|
struct IntWrapper2: _SwiftNewtypeWrapper, Hashable, _ObjectiveCBridgeable {
|
|
let rawValue: Int
|
|
}
|
|
|
|
AnyHashableCasts.test("Wrappers around bridged integers") {
|
|
let wrapper1: AnyHashable = IntWrapper1(rawValue: 42)
|
|
let wrapper2: AnyHashable = IntWrapper2(rawValue: 42)
|
|
let integer: AnyHashable = 42 as Int
|
|
let byte: AnyHashable = 42 as UInt8
|
|
let double: AnyHashable = 42.0 as Double
|
|
let number: AnyHashable = 42 as NSNumber
|
|
|
|
// Wrappers compare equal to their wrapped value as AnyHashable.
|
|
expectEqual(wrapper1, wrapper2)
|
|
expectEqual(wrapper1, integer)
|
|
expectEqual(wrapper1, byte)
|
|
expectEqual(wrapper1, double)
|
|
expectEqual(wrapper1, number)
|
|
|
|
// Original types are preserved in the base property.
|
|
expectTrue(wrapper1.base is IntWrapper1)
|
|
expectTrue(wrapper2.base is IntWrapper2)
|
|
expectTrue(integer.base is Int)
|
|
expectTrue(byte.base is UInt8)
|
|
expectTrue(double.base is Double)
|
|
expectTrue(number.base is NSNumber) // Through bridging
|
|
|
|
// AnyHashable forms can be casted to any standard numeric type that can hold
|
|
// their value.
|
|
expectNotNil(wrapper1 as? IntWrapper1)
|
|
expectNotNil(wrapper1 as? IntWrapper2)
|
|
expectNotNil(wrapper1 as? Int)
|
|
expectNotNil(wrapper1 as? UInt8)
|
|
expectNotNil(wrapper1 as? Double)
|
|
expectNotNil(wrapper1 as? NSNumber)
|
|
|
|
expectNotNil(byte as? IntWrapper1)
|
|
expectNotNil(byte as? IntWrapper2)
|
|
expectNotNil(byte as? Int)
|
|
expectNotNil(byte as? UInt8)
|
|
expectNotNil(byte as? Double)
|
|
expectNotNil(byte as? NSNumber)
|
|
|
|
expectNotNil(integer as? IntWrapper1)
|
|
expectNotNil(integer as? IntWrapper2)
|
|
expectNotNil(integer as? Int)
|
|
expectNotNil(integer as? UInt8)
|
|
expectNotNil(integer as? Double)
|
|
expectNotNil(integer as? NSNumber)
|
|
|
|
expectNotNil(double as? IntWrapper1)
|
|
expectNotNil(double as? IntWrapper2)
|
|
expectNotNil(double as? Int)
|
|
expectNotNil(double as? UInt8)
|
|
expectNotNil(double as? Double)
|
|
expectNotNil(double as? NSNumber)
|
|
|
|
expectNotNil(number as? IntWrapper1)
|
|
expectNotNil(number as? IntWrapper2)
|
|
expectNotNil(number as? Int)
|
|
expectNotNil(number as? UInt8)
|
|
expectNotNil(number as? Double)
|
|
expectNotNil(number as? NSNumber)
|
|
|
|
// We can't cast to a numeric type that can't hold the value.
|
|
let big: AnyHashable = Int32.max
|
|
expectNotNil(big as? IntWrapper1)
|
|
expectNotNil(big as? IntWrapper2)
|
|
expectNotNil(big as? Int)
|
|
expectNil(big as? UInt8) // <--
|
|
expectNotNil(big as? Double)
|
|
expectNotNil(big as? NSNumber)
|
|
}
|
|
|
|
// A wrapper type around a String that bridges to NSString.
|
|
struct StringWrapper1: _SwiftNewtypeWrapper, Hashable, _ObjectiveCBridgeable {
|
|
let rawValue: String
|
|
}
|
|
|
|
// A wrapper type around a String that bridges to NSString.
|
|
struct StringWrapper2: _SwiftNewtypeWrapper, Hashable, _ObjectiveCBridgeable {
|
|
let rawValue: String
|
|
}
|
|
|
|
AnyHashableCasts.test("Wrappers around bridged strings") {
|
|
let wrapper1Hello: AnyHashable = StringWrapper1(rawValue: "hello")
|
|
let wrapper2Hello: AnyHashable = StringWrapper2(rawValue: "hello")
|
|
let stringHello: AnyHashable = "hello" as String
|
|
let nsStringHello: AnyHashable = "hello" as NSString
|
|
|
|
// Wrappers compare equal to their wrapped value as AnyHashable.
|
|
expectEqual(wrapper1Hello, wrapper2Hello)
|
|
expectEqual(wrapper1Hello, stringHello)
|
|
expectEqual(wrapper1Hello, nsStringHello)
|
|
expectEqual(wrapper2Hello, stringHello)
|
|
expectEqual(wrapper2Hello, nsStringHello)
|
|
expectEqual(stringHello, nsStringHello)
|
|
|
|
// Type identity is maintained through the base property.
|
|
expectTrue(wrapper1Hello.base is StringWrapper1)
|
|
expectTrue(wrapper2Hello.base is StringWrapper2)
|
|
expectTrue(stringHello.base is String)
|
|
expectTrue(nsStringHello.base is NSString) // Through bridging
|
|
|
|
// Swift wrapper's AnyHashable form doesn't enfore type identity.
|
|
expectNotNil(wrapper1Hello as? StringWrapper1)
|
|
expectNotNil(wrapper1Hello as? StringWrapper2)
|
|
expectNotNil(wrapper1Hello as? String)
|
|
expectNotNil(wrapper1Hello as? NSString)
|
|
|
|
// String's AnyHashable form doesn't enfore type identity.
|
|
expectNotNil(stringHello as? StringWrapper1)
|
|
expectNotNil(stringHello as? StringWrapper2)
|
|
expectNotNil(stringHello as? String)
|
|
expectNotNil(stringHello as? NSString)
|
|
|
|
// NSString's AnyHashable form doesn't enfore type identity.
|
|
expectNotNil(nsStringHello as? StringWrapper1)
|
|
expectNotNil(nsStringHello as? StringWrapper2)
|
|
expectNotNil(nsStringHello as? String)
|
|
expectNotNil(nsStringHello as? NSString)
|
|
}
|
|
|
|
#endif
|
|
|
|
runAllTests()
|