How can I use a Swift enum as a Dictionary key? (Conforming to Equatable)
Solution 1
Info on Enumerations as dictionary keys:
From the Swift book:
Enumeration member values without associated values (as described in Enumerations) are also hashable by default.
However, your Enumeration does have a member value with an associated value, so Hashable
conformance has to be added manually by you.
Solution
The problem with your implementation, is that operator declarations in Swift must be at a global scope.
Just move:
func == (lhs: StationSelector, rhs: StationSelector) -> Bool {
return lhs.toInt() == rhs.toInt()
}
outside the enum
definition and it will work.
Check the docs for more on that.
Solution 2
I struggled for a little trying to make an enum
with associated values conform to Hashable
.
Here's I made my enum
with associated values conform to Hashable
so it could be sorted or used as a Dictionary
key, or do anything else that Hashable
can do.
You have to make your associated values enum
conform to Hashable
because associated values enums
cannot have a raw type.
public enum Components: Hashable {
case None
case Year(Int?)
case Month(Int?)
case Week(Int?)
case Day(Int?)
case Hour(Int?)
case Minute(Int?)
case Second(Int?)
///The hashValue of the `Component` so we can conform to `Hashable` and be sorted.
public var hashValue : Int {
return self.toInt()
}
/// Return an 'Int' value for each `Component` type so `Component` can conform to `Hashable`
private func toInt() -> Int {
switch self {
case .None:
return -1
case .Year:
return 0
case .Month:
return 1
case .Week:
return 2
case .Day:
return 3
case .Hour:
return 4
case .Minute:
return 5
case .Second:
return 6
}
}
}
Also need to override the equality operator:
/// Override equality operator so Components Enum conforms to Hashable
public func == (lhs: Components, rhs: Components) -> Bool {
return lhs.toInt() == rhs.toInt()
}
Solution 3
For more readability, let's reimplement StationSelector
with Swift 3:
enum StationSelector {
case nearest, lastShown, list, specific(Int)
}
extension StationSelector: RawRepresentable {
typealias RawValue = Int
init?(rawValue: RawValue) {
switch rawValue {
case -1: self = .nearest
case -2: self = .lastShown
case -3: self = .list
case (let value) where value >= 0: self = .specific(value)
default: return nil
}
}
var rawValue: RawValue {
switch self {
case .nearest: return -1
case .lastShown: return -2
case .list: return -3
case .specific(let value) where value >= 0: return value
default: fatalError("StationSelector is not valid")
}
}
}
The Apple developer API Reference states about Hashable
protocol:
When you define an enumeration without associated values, it gains
Hashable
conformance automatically, and you can addHashable
conformance to your other custom types by adding a singlehashValue
property.
Therefore, because StationSelector
implements associated values, you must make StationSelector
conform to Hashable
protocol manually.
The first step is to implement ==
operator and make StationSelector
conform to Equatable
protocol:
extension StationSelector: Equatable {
static func == (lhs: StationSelector, rhs: StationSelector) -> Bool {
return lhs.rawValue == rhs.rawValue
}
}
Usage:
let nearest = StationSelector.nearest
let lastShown = StationSelector.lastShown
let specific0 = StationSelector.specific(0)
// Requires == operator
print(nearest == lastShown) // prints false
print(nearest == specific0) // prints false
// Requires Equatable protocol conformance
let array = [nearest, lastShown, specific0]
print(array.contains(nearest)) // prints true
Once Equatable
protocol is implemented, you can make StationSelector
conform to Hashable
protocol:
extension StationSelector: Hashable {
var hashValue: Int {
return self.rawValue.hashValue
}
}
Usage:
// Requires Hashable protocol conformance
let dictionnary = [StationSelector.nearest: 5, StationSelector.lastShown: 10]
The following code shows the required implementation for StationSelector
to make it conform to Hashable
protocol using Swift 3:
enum StationSelector: RawRepresentable, Hashable {
case nearest, lastShown, list, specific(Int)
typealias RawValue = Int
init?(rawValue: RawValue) {
switch rawValue {
case -1: self = .nearest
case -2: self = .lastShown
case -3: self = .list
case (let value) where value >= 0: self = .specific(value)
default: return nil
}
}
var rawValue: RawValue {
switch self {
case .nearest: return -1
case .lastShown: return -2
case .list: return -3
case .specific(let value) where value >= 0: return value
default: fatalError("StationSelector is not valid")
}
}
static func == (lhs: StationSelector, rhs: StationSelector) -> Bool {
return lhs.rawValue == rhs.rawValue
}
var hashValue: Int {
return self.rawValue.hashValue
}
}
Doug Knowles
Programming since the days of "wooden computers and iron programmers" (well, not quite: http://alum.wpi.edu/~trb/hacker70s.html).
Updated on February 28, 2020Comments
-
Doug Knowles over 4 years
I've defined an enum to represent a selection of a "station"; stations are defined by a unique positive integer, so I've created the following enum to allow negative values to represent special selections:
enum StationSelector : Printable { case Nearest case LastShown case List case Specific(Int) func toInt() -> Int { switch self { case .Nearest: return -1 case .LastShown: return -2 case .List: return -3 case .Specific(let stationNum): return stationNum } } static func fromInt(value:Int) -> StationSelector? { if value > 0 { return StationSelector.Specific(value) } switch value { case -1: return StationSelector.Nearest case -2: return StationSelector.LastShown case -3: return StationSelector.List default: return nil } } var description: String { get { switch self { case .Nearest: return "Nearest Station" case .LastShown: return "Last Displayed Station" case .List: return "Station List" case .Specific(let stationNumber): return "Station #\(stationNumber)" } } } }
I'd like to use these values as keys in a dictionary. Declaring a Dictionary yields the expected error that StationSelector doesn't conform to Hashable. Conforming to Hashable is easy with a simple hash function:
var hashValue: Int { get { return self.toInt() } }
However,
Hashable
requires conformance toEquatable
, and I can't seem to define the equals operator on my enum to satisfy the compiler.func == (lhs: StationSelector, rhs: StationSelector) -> Bool { return lhs.toInt() == rhs.toInt() }
The compiler complains that this is two declarations on a single line and wants to put a
;
afterfunc
, which doesn't make sense, either.Any thoughts?
-
jedwidz over 5 yearsFrom Swift 4.1 due to SE-0185, Swift also supports synthesizing
Equatable
andHashable
for enums with associated values. -
jedwidz over 5 yearsFor custom
==
, the implementation can alternatively be placed in the body of theenum
, provided it's static:static func ==(...
. -
AmitaiB over 5 yearsSwift Book quote from The Swift Programming Language, p. 186 give or take.
-
Peter Schorn almost 4 yearsThis is absolutely wrong. Why is this marked as the correct answer? operator functions (not "operator declarations"—no new operator is being declared) can be static members of the type. They don't have to be at the global scope.
-
Cezar almost 4 years@PeterSchorn it's a 6 years old answer :). Anyone is welcome to suggest an edit or add a new answer with a more up to date solution.
-
jjramos over 3 yearsHow would you use an enum with an associated value as a Dictionary key? In this case, could you expand your usage example to show how you would use
StationSelector.specific
as a key in that Dictionary?