Swift: Hashable struct with dictionary property
I think you need to review your data model if you have to use a whole struct as a dictionary key. Anyhow, here's one way to do it:
internal struct MapKey: Hashable {
internal let id: String
internal let values: [String:String]
var hashValue: Int {
get {
var hashString = self.id + ";"
for key in values.keys.sort() {
hashString += key + ";" + values[key]!
}
return hashString.hashValue
}
}
}
func ==(lhs: MapKey, rhs: MapKey) -> Bool {
return lhs.id == rhs.id && lhs.values == rhs.values
}
This assumes that you don't have semicolon (;
) in id
or in the keys and values of values
. Hasable
implies Equatable
so you don't need to declare it conforming to Equatable
again.
Hashable struct with interchangeable properties?
This is how Hasher works
https://developer.apple.com/documentation/swift/hasher
However, the underlying hash algorithm is designed to exhibit
avalanche effects: slight changes to the seed or the input byte
sequence will typically produce drastic changes in the generated hash
value.
So the problem here in hash(into:) func
Since the sequence matters combine
operation is not commutative. You should find some other function to be a hash for this struct. In your case the best option is
func hash(into hasher: inout Hasher) {
hasher.combine(leftOperand & rightOperand)
}
As @Martin R pointed out to have less collisions it's better to use ^
func hash(into hasher: inout Hasher) {
hasher.combine(leftOperand ^ rightOperand)
}
Make struct Hashable?
Simply return dbName.hashValue
from your hashValue
function. FYI - the hash value does not need to be unique. The requirement is that two objects that equate equal must also have the same hash value.
struct PetInfo: Hashable {
var petName: String
var dbName: String
var hashValue: Int {
return dbName.hashValue
}
static func == (lhs: PetInfo, rhs: PetInfo) -> Bool {
return lhs.dbName == rhs.dbName && lhs.petName == rhs.petName
}
}
Conforming to Hashable protocol?
You're missing the declaration:
struct DateStruct: Hashable {
And your ==
function is wrong. You should compare the three properties.
static func == (lhs: DateStruct, rhs: DateStruct) -> Bool {
return lhs.year == rhs.year && lhs.month == rhs.month && lhs.day == rhs.day
}
It's possible for two different values to have the same hash value.
How to handle hash collisions for Dictionaries in Swift
func ==(lhs: MyStructure, rhs: MyStructure) -> Bool {
return lhs.hashValue == rhs.hashValue
}
Note the global function to overload the equality operator (==) in order to conform to the Equatable Protocol, which is required by the Hashable Protocol.
Your problem is an incorrect equality implementation.
A hash table (such as a Swift Dictionary or Set) requires separate equality and hash implementations.
hash gets you close to the object you're looking for; equality gets you the exact object you're looking for.
Your code uses the same implementation for hash and equality, and this will guarantee a collision.
To fix the problem, implement equality to match exact object values (however your model defines equality). E.g.:
func ==(lhs: MyStructure, rhs: MyStructure) -> Bool {
return lhs.id == rhs.id
}
Struct hash for different types
You can make a protocol containing all the shared data, so both S1
and S2
go through the same hasher.
I created a SharedData
protocol containing the shared properties (only data
in this case) and gave it a Hashable
implementation. Then S1
and S2
now conform to SharedData
. It looks like this:
protocol SharedData: Hashable {
var data: String { get }
}
extension SharedData {
func hash(into hasher: inout Hasher) {
hasher.combine(data)
}
}
struct S1: SharedData {
let data: String
let otherData: String
// ... Equality & Hash functions, both only use data
}
struct S2: SharedData {
let data: String
let otherData: Int
// ... Equality & Hash functions, both only use data
}
And the comparison looks like this:
print(S1(data: "hi", otherData: "there").hashValue)
print(S2(data: "hi", otherData: 1).hashValue)
let set: Set<Int> = [S1(data: "Hello", otherData: "world!").hashValue, S1(data: "abc", otherData: "123").hashValue]
let obj2 = S2(data: "Hello", otherData: 0)
print(set.contains(obj2.hashValue))
Prints:
-5068166682035457964 // <- these change every program execution
-5068166682035457964
true
Unfortunately it's not possible to do the following, yet:
let set: Set<SharedData> = [S1(data: "Hello", otherData: "world!"), S1(data: "abc", otherData: "123")]
Error:
Protocol 'SharedData' as a type cannot conform to 'Hashable'
I believe SE-0309 (Unlock existentials for all protocols) could fix this.
What is the use of hashable protocol in swift4?
To make an object conform to Hashable we need to provide a hashValue property that will return a unique, consistent number for each instance.
The Hashable protocol inherits from Equatable, so you may also need to implement an == function.
Note: If two objects compare as equal using == they should also generate the same hash value, but the reverse isn’t true – hash collisions can happen.
Before Swift 4.1, conforming to Hashable was complex because you needed to calculate a hashValue property by hand.
In Swift 4.1 this improved so that hashValue could be synthesized on your behalf if all the properties conform to Hashable .
Swift 4.2 introduces a new Hasher struct that provides a randomly seeded, universal hash function to make all our lives easier. Refer for more
Why does struct need to conform to Hashable as well as Generic array when converting to a dictionary
There's no need to make CustomSet
conform to Hashable
. Simply adding the Hashable
restriction to the generic type parameter solves the error
Cannot subscript a value of incorrect or ambiguous type
,which is expected, since a Dictionary
's keys need to conform to Hashable
.
Btw the syntax for declaring conformance to several protocols is &
, not ,
, so there's no need for the where clause.
struct CustomSet<T : Comparable & Hashable> {
private var list: [T]
init(_ list: [T]){
let uniqueKeys = list.reduce(into: [:]){ dict, item in
dict[item, default: 0] += 1
}.keys
self.list = Array(uniqueKeys)
}
}
extension CustomSet : Equatable {
static func == (lhs: CustomSet, rhs: CustomSet) -> Bool {
return lhs.list.count == rhs.list.count && lhs.list.sorted() == rhs.list.sorted()
}
}
Moreover, you can simply do self.list = Array(Set(list))
in the initializer to store only the unique elements from the input in your list
, there's no need for messing around with a Dictionary
.
Related Topics
Context Menu Not Updating in Swiftui
Swift Get Nsdata of a Video from Photos Library
Curl with Alamofire - Swift - Multipart/Form-Data
Swift Combine Publishers VS Completion Handler and When to Cancel
Swiftui Widget iOS 14 Gradient Issue
Swift; Delegate Embedded View Controller and Parent
Struggling with Notificationcenter/Combine in Swiftui/Avplayer
Generate Avaudiopcmbuffer with Avaudiorecorder
Testing Protocol Conformance with Associated Types
Protocol Extension Initializer Forcing to Call Self.Init
Setting Arkit Orientation via Swift
Captureoutput Function Isn't Called Using Setsamplebufferdelegate
How to Cast a Metaclass Object to a Protocol Type in Swift
Swift Combine: Using Timer Publisher in an Observable Object