Hashable - overriding hashValue

I had the following under Swift 4.2:

public enum DataConversionOption {
    case encoding(String.Encoding)
    case endian(Endian)
    case nbrFromString
}


extension DataConversionOption: Hashable {
    public var hashValue: Int { 
        switch self { 
        case .encoding: return 0
        case .endian: return 1 
        case .nbrFromString: return 2
        }

    }

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

It still works under Swift 5.0, but gives the following warning:

/mnt/d/src/RecordProperyMap/Sources/RecordProperyMap/DataConversionOption.swift:9:16: warning: 'Hashable.hashValue' is deprecated as a protocol requirement; conform type 'DataConversionOption' to 'Hashable' by implementing 'hash(into:)' instead
    public var hashValue: Int {
               ^

How might I implement a hash(into:) to method resulting in the same behavior?

Essentially, I have a Set that I want to contain only a single instance, at most, of each enum case; ignoring the associated value when making this determination.

Should I just ignore hashValue and override == to compare the enumerations directly? (Just thought of that; haven't tried it yet.)

Am I totally misusing this feature and should look for another way to accomplish my goal?

So this seems to work.

extension DataConversionOption: Hashable {
    /* ignore associated values when determining equality */
    public static func == (lhs: DataConversionOption, rhs: DataConversionOption) -> Bool {
        switch (lhs, rhs) {
        case (.encoding, .encoding): return true
        case (.endian, .endian): return true
        case (.nbrFromString, .nbrFromString): return true
        default: return false
        }
    }
}

Not too bad. Any reason I shouldn't do this?

When I put that in a playground, I get 'DataConversionOption' does not conform to protocol 'Hashable'. That goes away if Endian is Hashable and in that case you are relying on the compiler synthesizing Hashable for you.

Borrowing from a more knowledgable poster:

Your implementation violates this, because the automatically synthesized Hashable takes the associated values into account - DataConversionOption.encoding(.ascii).hashValue != DataConversionOption.encoding(.utf8).hashValue even though they are equal as far as Equatable is concerned.

That said, it seems to work in Sets, so :man_shrugging:

I believe the correct way to implement hashing now (the way you want it to work) would be

public func hash(into hasher: inout Hasher) {
    switch self {
        case .encoding:      hasher.combine(0)
        case .endian:        hasher.combine(1)
        case .nbrFromString: hasher.combine(2)
    }
}
2 Likes

I believe that @sjavora has already answered your question, but if you'd like to know a bit more about how Swift automatically synthesizes Hashable conformance you may find this thread informative:

There's even a section on this topic in Adopting Common Protocols:

Use All Significant Properties for Equatable and Hashable

When implementing the == method and the hash(into:) method, use all the properties that affect whether two instances of your custom type are considered equal. In the implementations above, the Player type uses name and position in both methods.

If your type contains properties that don't affect whether two instances are considered equal, exclude those properties from comparison in the == method and from hashing in hash(into:) . For example, a type might cache an expensive computed value so that it only needs to calculate it once. If you compare two instances of that type, whether or not the computed value has been cached shouldn't affect their equality, so the cached value should be excluded from comparison and hashing.


Important

Always use the same properties in both your == and hash(into:) methods. Using different groups of properties in the two methods can lead to unexpected behavior or performance when using your custom type in sets and dictionaries.


Here is the best way to implement the Hashable conformance, as long as Endian is Hashable:

extension DataConversionOption: Hashable {
  // hash(into:) and == are automatically synthesized by the compiler.
}

Unless you're doing something unusual, the best option is to not write any code.

For reference, here is the code that the compiler generates for you. This code properly compares and hashes all information contained in a DataConversionOption value, including the enum's associated values.

extension DataConversionOption: Equatable {
  static func == (left: DataConversionOption, right: DataConversionOption) -> Bool {
    switch (left, right) {
    case let (.encoding(e1), .encoding(e2)): return e1 == e2
    case let (.endian(e1), .endian(e2)): return e1 == e2
    case let (.nbrFromString, .nbrFromString): return true
    default: return false
    }
  }
}

extension DataConversionOption: Hashable {
  func hash(into hasher: inout Hasher) {
    switch self {
    case .encoding(let encoding):
      hasher.combine(0)
      hasher.combine(encoding)
    case .endian(let endian):
      hasher.combine(1)
      hasher.combine(endian)
    case .nbrFromString:
      hasher.combine(2)
    }
  }
} 

Update: If for some reason you really don't want to compare associated values in the .encoding and .endian cases, then you'll need to manually provide definitions for both == and hash(into:). Your == along with @sjavora's hash(into:) will work great in that case. (You definitely do need to provide both, or hashing won't work properly.)

3 Likes

I do want special behavior, because I want the associated values to be ignored in the comparision.

This is what I have now, and it seems to do what I need.

public enum DataConversionOption {
    case encoding(String.Encoding)
    case endian(Endian)
    case nbrFromString
}

extension DataConversionOption: Hashable {
    /* ignore associated values when determining equality */
    public static func == (lhs: DataConversionOption, rhs: DataConversionOption) -> Bool {
        switch (lhs, rhs) {
        case (.encoding, .encoding): return true
        case (.endian, .endian): return true
        case (.nbrFromString, .nbrFromString): return true
        default: return false
        }
    }

    public func hash(into hasher: inout Hasher) {
        let val: Int
        switch self {
        case .encoding: val = 0
        case .endian: val = 1
        case .nbrFromString: val = 2
        }
        hasher.combine(val)
    } 
}

Thanks for all of the answers.

1 Like