37

I'm trying to make a dictionary with the key as a struct I've created and the value as an array of Ints. However, I keep getting the error:

Type 'DateStruct' does not conform to protocol 'Hashable'

I'm pretty sure I've implemented the necessary methods but for some reason it still doesn't work.

Here's my struct with the implemented protocols:

struct DateStruct {
    var year: Int
    var month: Int
    var day: Int

    var hashValue: Int {
        return (year+month+day).hashValue
    }

    static func == (lhs: DateStruct, rhs: DateStruct) -> Bool {
        return lhs.hashValue == rhs.hashValue
    }

    static func < (lhs: DateStruct, rhs: DateStruct) -> Bool {
        if (lhs.year < rhs.year) {
            return true
        } else if (lhs.year > rhs.year) {
            return false
        } else {
            if (lhs.month < rhs.month) {
                return true
            } else if (lhs.month > rhs.month) {
                return false
            } else {
                if (lhs.day < rhs.day) {
                    return true
                } else {
                    return false
                }
            }
        }
    }
}

Can anybody please explain to me why I'm still getting the error?

pkamb
  • 33,281
  • 23
  • 160
  • 191
MarksCode
  • 8,074
  • 15
  • 64
  • 133

7 Answers7

41

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.

pkamb
  • 33,281
  • 23
  • 160
  • 191
rmaddy
  • 314,917
  • 42
  • 532
  • 579
  • 1
    Thankyou! Btw, why is the `==` wrong? Wouldn't 2 dates with the same day, month, and year have the same hashValue anyways? – MarksCode Feb 22 '17 at 04:59
  • Of course two equal dates will have the same hash. But two dates with the same hash are not necessarily equal. Two different dates can have the same hash. That's fine. But two dates with the same hash don't have to be equal. – rmaddy Feb 22 '17 at 05:01
  • Well Im confused, anyways is my `return (year+month+day).hashValue` not sufficient since 2 different date's year, month, and day could add up to the same thing like 2000, 1, 2 and 2000, 2, 1? – MarksCode Feb 22 '17 at 05:06
  • Your implementation of `hashValue` is fine. It would be correct to simply return 42 if you wanted (but please don't). From the `Hashable` docs: *"A hash value, provided by a type’s hashValue property, is an integer that is the same for any two instances that compare equally. That is, for two instances a and b of the same type, if a == b then a.hashValue == b.hashValue. The reverse is not true: Two instances with equal hash values are not necessarily equal to each other."* That's why you need to fix your `==` function. – rmaddy Feb 22 '17 at 05:09
  • Oh I see, so the combination of the `hashValue` and `==` makes comparing work. Thanks! – MarksCode Feb 22 '17 at 05:11
  • Not really. `==` makes comparing work. You can have comparison without hashing. `hashValue` makes values "hashable" which allows them to be used as dictionary keys or to be stored in some other form of a hash map/table. Note that `Hashable` extends `Equatable`. This means that hashing depends on comparing but comparing does not depend on hashing. Read the docs for those two protocols for further details. – rmaddy Feb 22 '17 at 05:15
  • Oh, so thats why if I say `dict[DateStruct1] = 1` and `dict[DateStruct2] = 2` it wouldn't be accessing the same dictionary entry even if `DateStruct1` and `DateStruct2` might have the same hashValue, it would also compare them using `==`. – MarksCode Feb 22 '17 at 05:20
18

While

    var hashValue: Int 

is still applicable in the legacy NSObject inheritance trees it's otherwise obsolete.

    func hash(into hasher: inout Hasher)
    {
        hasher.combine(year);
        hasher.combine(month) 
    ...

is the swiftlely modern way to hash your way in class.

And per rmaddy answer above "==" operator also has to have kinks ironed out to be correct for your semantics.

Per Manish you get Hashable comformance for structs for free just declaring that.

Anton Tropashko
  • 5,486
  • 5
  • 41
  • 66
  • What's the benefit of this approach over e.g. @rmaddy's accepted answer from three years ago? Under what conditions might someone prefer your suggestion? – Jeremy Caney Jun 05 '20 at 00:12
  • 1
    This is not a question of a benefit: maddy answer is required but not sufficient to satisfy Hashable requirements. Once you add == the error remains but xcode 11.3 through 11.5 would NOT offer you to create protocol stub for hash(into – Anton Tropashko Jun 05 '20 at 09:30
  • 1
    Swift will generate a hash(into:) method automatically if the struct conforms to the Hashable protocol and all properties also conform to the Hashable protocol (Swift 5.4 and Xcode 13.2.1) – Manish Nahar May 09 '22 at 09:23
9

If the class has fields of type (another class), that class should adopt Hashable.

example

struct Project : Hashable {
    var activities: [Activity]?

}

Here, Activity class must adopt Hashable too.

Hatim
  • 1,516
  • 1
  • 18
  • 30
6

If you don't want to use the hashValue, you can combine the hash of your values with the hash(into:) method.

For more information see the answer : https://stackoverflow.com/a/55118328/1261547

Lilo
  • 2,724
  • 25
  • 16
3

For simple structs, where all its properties are already Hashable (i.e. Int, String, ... ), we can conform to Hashable just declaring it (see https://developer.apple.com/documentation/swift/hashable )

So no need to implement hashValue (which btw is deprecated) nor == (because Hashable conforms to Equatable).

And since we're implementing the < operator, then it'd make sense to conform to Comparable, so we can sort (i.e. [dateStructA, dateStructB, ...].sorted()).

So I'd do it like:

struct DateStruct: Comparable & Hashable {
    let year: Int
    let month: Int
    let day: Int

    static func < (lhs: DateStruct, rhs: DateStruct) -> Bool {
        if lhs.year != rhs.year {
           return lhs.year < rhs.year
        } else if lhs.month != rhs.month {
           return lhs.month < rhs.month
        } else {
           return lhs.day < rhs.day
        }
    }
}
FranMowinckel
  • 4,233
  • 1
  • 30
  • 26
2

You did not specified the Hashable protocol when defining struct:

struct DateStruct: Hashable { ...

The following code is from your example and it runs on a Playground. Please note that your == operator has been modified here:

import Foundation

struct DateStruct: Hashable {
    var year: Int
    var month: Int
    var day: Int

    var hashValue: Int {
        return (year+month+day).hashValue
    }

    static func == (lhs: DateStruct, rhs: DateStruct) -> Bool {
        return lhs.year == rhs.year && lhs.month == rhs.month && lhs.day == rhs.day
    }

    static func < (lhs: DateStruct, rhs: DateStruct) -> Bool {
        if (lhs.year < rhs.year) {
            return true
        } else if (lhs.year > rhs.year) {
            return false
        } else {
            if (lhs.month < rhs.month) {
                return true
            } else if (lhs.month > rhs.month) {
                return false
            } else {
                if (lhs.day < rhs.day) {
                    return true
                } else {
                    return false
                }
            }
        }
    }
}

var d0 = DateStruct(year: 2017, month: 2, day: 21)
var d1 = DateStruct(year: 2017, month: 2, day: 21)

var dates = [DateStruct:Int]()
dates[d0] = 23
dates[d1] = 49

print(dates)

print(d0 == d1) // true

d0.year = 2018

print(d0 == d1) // false
pkamb
  • 33,281
  • 23
  • 160
  • 191
nbloqs
  • 3,152
  • 1
  • 28
  • 49
1
public struct HistoryFilePair : Hashable {
    //this
    public static func == (lhs: HistoryFilePair, rhs: HistoryFilePair) -> Bool {
        lhs.commit.oidShort == rhs.commit.oidShort
    }
    
    // this
    public func hash(into hasher: inout Hasher) {
        return hasher.combine(self.commit.oidShort)
    }

    //some code of your struct/class
}
Andrew_STOP_RU_WAR_IN_UA
  • 9,318
  • 5
  • 65
  • 101