I already covered making a custom Swift type Equatable
and Comparable
which allows us to test if an Array
contains an instance of our type or to sort the array amongst other things. What if we want to store our type in a Set
or Dictionary
?
Update 2 April 2018: Starting with Swift 4.1 you can have the compiler automatically synthesize Hashable
conformance for your type. See How To Get Equatable And Hashable For Free.
Making Our Custom Type Hashable
To use a type in a Set
or Dictionary
it has to be Hashable
which means providing a hashValue
property. Your type must also be Equatable
which I covered in Swift Equatable and Comparable. To recap our Swift Country
type looks like this:
struct Country {
let name: String
let capital: String
var visited: Bool
}
extension Country: Equatable {
static func == (lhs: Country, rhs: Country) -> Bool {
return lhs.name == rhs.name &&
lhs.capital == rhs.capital &&
lhs.visited == rhs.visited
}
}
The hashValue
property is an integer that must be the same for any two instances of the type that are equal. Swift standard library types such as String
, Integer
, Bool
are all Hashable
.
// hash values can vary
let hello = "hello"
let world = "world"
hello.hashValue // 4799432177974197528
"\(hello) \(world)".hashValue // 3658945855109305670
"hello world".hashValue // 3658945855109305670
Starting with Swift 4.1 the easiest way to make our type hashable is to have the compiler automatically synthesise conformance for us (see How To Get Equatable And Hashable For Free).
As long as all of the properties of our struct are themselves hashable it’s enough to declare conformance and the compiler does the rest:
extension Country: Hashable {}
let uk = Country(name: "United Kingdom", capital: "London", visited: true)
uk.hashValue // -8344213515631337230 (actual value will vary)
To understand what the compiler is doing we can manually implement the hash(into:)
method:
extension Country: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(name)
hasher.combine(capital)
hasher.combine(visited)
}
}
We just feed each of the essential components of our type into the combine
function of the hasher
provided by the standard library. Note that these are the same components we use when implementing Equatable
.
We can now use Country
in a Set
or Dictionary
:
let destinations: Set = [belgium,canada,brazil]
let europe: Set = [belgium,france,uk]
destinations.intersection(europe) // belgium
let counts = [uk: 1000, france: 2000]
counts[uk] // 1000
Notes:
-
Do not assume that two instances of a type with the same hash value are equal. Depending on how we compute the hash value we can get collisions where two different instances share the same hash value. The
Hashable
protocol only needs the reverse - two equal instances have the same hash value. -
The hash value of a Bool from the Swift Standard Library happens to be either 0 (false) or 1 (true) - but don’t depend on those values.
-
There is no guarantee that a hash value will be the same for different executions of your code.