我该如何将Swift枚举用作字典键?(遵守Equatable协议)

42

我已经定义了一个枚举类型来表示“站点”的选择;站点由唯一的正整数定义,因此我创建了以下枚举类型,允许负值表示特殊选择:

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)"
        }
    }
    }
}

我希望将这些值用作字典中的键。声明一个字典会产生一个预期的错误,即StationSelector不符合Hashable。通过一个简单的哈希函数符合Hashable很容易:

var hashValue: Int {
get {
    return self.toInt()
}
}

然而,Hashable要求符合Equatable,但我似乎无法定义枚举类型的等于运算符以满足编译器的要求。

func == (lhs: StationSelector, rhs: StationSelector) -> Bool {
    return lhs.toInt() == rhs.toInt()
}
编译器抱怨这是单行上的两个声明,并想在func之后放置一个;,但这毫无意义。你有什么想法吗?

2
Swift 中,操作符必须在文件范围内定义。 - holex
枚举类型已经是可比较的。 - matt
难道不应该是 @infix func == 吗? - Kreiri
@matt 看看我的答案。这仅适用于没有相关值的成员值的枚举。 - Cezar
2
@Cezar 很好的发现,感谢您的更正。 - matt
5个回答

25

枚举作为字典键的信息:

来自Swift官方文档:

枚举成员值没有关联值(如“枚举”中所述),默认情况下也是可哈希的。

但是,如果您的枚举具有具有关联值的成员值,则必须手动添加Hashable一致性。

解决方案

您的实现问题在于,在Swift中,运算符声明必须位于全局范围内。

请将以下代码移动到全局范围内即可解决问题:

func == (lhs: StationSelector, rhs: StationSelector) -> Bool {
    return lhs.toInt() == rhs.toInt()
}

enum定义外部使用该方法也可以正常工作。

更多信息请查看文档


1
从Swift 4.1开始,由于SE-0185,Swift还支持为具有关联值的枚举合成EquatableHashable - jedwidz
对于自定义的 ==,实现可以放在 enum 的主体中,只要它是静态的:static func ==(... - jedwidz
这是绝对错误的。为什么将其标记为正确答案?操作符函数(不是“操作符声明”——没有新的操作符被声明)可以是类型的静态成员。它们不必在全局范围内。 - Peter Schorn
@PeterSchorn 这是一个六年前的答案 :). 任何人都可以建议编辑或添加一个更加现代化的解决方案。 - Cezar

6

我曾经尝试让一个带有关联值的enum符合Hashable协议,但是遇到了一些困难。

以下是我如何让带有关联值的enum符合Hashable协议,从而使其可以进行排序或用作Dictionary键,或者执行任何其他Hashable所能做的操作。

你必须让你的关联值enum符合Hashable协议,因为关联值enums不能具有原始类型。

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
        }

    }

}

还需要重载等于运算符:

/// Override equality operator so Components Enum conforms to Hashable
public func == (lhs: Components, rhs: Components) -> Bool {
    return lhs.toInt() == rhs.toInt()
}

3
为了更好的可读性,让我们使用Swift 3重新实现StationSelector:
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")
        }
    }

}

苹果开发者API参考文档关于Hashable协议的说明:
当您定义一个不带关联值的枚举时,它会自动获得Hashable符合性。通过添加单个hashValue属性,您可以将Hashable符合性添加到其他自定义类型中。
因此,由于StationSelector实现了关联值,您必须手动使StationSelector符合Hashable协议。
第一步是实现 == 运算符并使 StationSelector 符合 Equatable 协议:
extension StationSelector: Equatable {

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

}

使用方法:

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

一旦实现了 Equatable 协议,您就可以使 StationSelector 符合 Hashable 协议。
extension StationSelector: Hashable {

    var hashValue: Int {
        return self.rawValue.hashValue
    }

}

使用方法:

// Requires Hashable protocol conformance
let dictionnary = [StationSelector.nearest: 5, StationSelector.lastShown: 10]

以下代码展示了使用Swift 3使StationSelector符合Hashable协议所需的实现方式:
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
    }

}

你如何使用带有关联值的枚举作为字典键?在这种情况下,你能否扩展你的用例示例,展示如何将 StationSelector.specific 用作该字典中的键? - jjramos

2
从Swift 5.5开始,您可以直接编写enum StationSelector: Hashable {}。 自动符合性将被合成。

0

强调一下Cezar之前说的话。如果你可以避免使用成员变量,你就不需要实现等于运算符来使枚举可哈希化 - 只需给它们一个类型!

enum StationSelector : Int {
    case Nearest = 1, LastShown, List, Specific
    // automatically assigned to 1, 2, 3, 4
}

这就是你所需要的。现在你也可以使用rawValue来初始化它们或稍后检索它。

let a: StationSelector? = StationSelector(rawValue: 2) // LastShown
let b: StationSelector = .LastShown

if(a == b)
{
    print("Selectors are equal with value \(a?.rawValue)")
}

如需更多信息,请查看文档


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接