Как я могу использовать перечисление Swift в качестве ключа словаря? (в соответствии с Equatable)

Я определил перечисление для представления выбора «станции»; станции определяются уникальным положительным целым числом, поэтому я создал следующее перечисление, чтобы отрицательные значения представляли специальные выборки:

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

Я хотел бы использовать эти значения в качестве ключей в словаре. Объявление Dictionary приводит к ожидаемой ошибке, что StationSelector не соответствует Hashable. Соответствовать Hashable легко с помощью простой хеш-функции:

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

Однако Hashable требует соответствия Equatable, и я не могу определить оператор равенства в своем перечислении, чтобы удовлетворить компилятор.

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

Компилятор жалуется, что это два объявления в одной строке, и хочет поставить ; после func, что тоже не имеет смысла.

Какие-нибудь мысли?


person Doug Knowles    schedule 03.07.2014    source источник
comment
операторы должны быть определены в файловой области в Swift.   -  person holex    schedule 04.07.2014
comment
Счетчики уже приравниваемы.   -  person matt    schedule 04.07.2014
comment
Разве это не должно быть @infix func == ?   -  person Kreiri    schedule 04.07.2014
comment
@матовый, посмотри мой ответ. Это верно только для перечислений без значения члена со связанным значением.   -  person Cezar    schedule 04.07.2014
comment
@Cezar Хороший улов, спасибо за исправление.   -  person matt    schedule 04.07.2014


Ответы (4)


Информация о перечислениях в качестве ключей словаря:

Из книги Свифт:

Значения элементов перечисления без связанных значений (как описано в разделе Перечисления) также по умолчанию хэшируются.

Однако ваше перечисление имеет значение члена со связанным значением, поэтому Hashable соответствие должно быть добавлено вами вручную.

Решение

Проблема с вашей реализацией заключается в том, что объявления операторов в Swift должны иметь глобальную область действия.

Просто двигайся:

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

вне определения enum, и это сработает.

Подробнее об этом читайте в документах. .

person Cezar    schedule 03.07.2014
comment
Из Swift 4.1 из-за SE-0185 Swift также поддерживает синтез Equatable и Hashable для перечислений со связанными значениями. - person jedwidz; 22.11.2018
comment
Для пользовательского == реализацию можно поместить в тело enum, если оно статично: static func ==(.... - person jedwidz; 22.11.2018
comment
Цитата из Swift Book из Язык программирования Swift, с. 186 плюс-минус. - person AmitaiB; 26.12.2018
comment
Это абсолютно неправильно. Почему это отмечено как правильный ответ? операторные функции (не объявления операторов — новый оператор не объявляется) могут быть статическими членами типа. Они не должны быть в глобальной области видимости. - person Peter Schorn; 25.08.2020
comment
@PeterSchorn это ответ 6-летней давности :). Любой может предложить редактирование или добавить новый ответ с более современным решением. - person Cezar; 28.08.2020

Я немного потрудился, пытаясь сделать так, чтобы 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()
}
person Sakiboy    schedule 03.07.2015

Для большей удобочитаемости давайте повторно реализуем StationSelector с помощью 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")
        }
    }

}

В справочнике по API для разработчиков Apple говорится о протоколе Hashable:

Когда вы определяете перечисление без связанных значений, оно автоматически получает соответствие Hashable, и вы можете добавить соответствие Hashable другим вашим пользовательским типам, добавив одно свойство hashValue.

Поэтому, поскольку 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]

В следующем коде показана необходимая реализация для StationSelector, чтобы он соответствовал протоколу Hashable с использованием 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
    }

}
person Imanou Petit    schedule 26.02.2017
comment
Как бы вы использовали перечисление со связанным значением в качестве ключа словаря? В этом случае не могли бы вы расширить свой пример использования, чтобы показать, как вы будете использовать StationSelector.specific в качестве ключа в этом словаре? - person jjramos; 25.01.2021

Просто для того, чтобы подчеркнуть то, что Цезарь сказал раньше. Если вы можете избежать использования переменной-члена, вам не нужно реализовывать оператор равенства, чтобы сделать перечисления хешируемыми — просто дайте им тип!

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

Для получения дополнительной информации ознакомьтесь с документацией.

person Sebastian Hojas    schedule 20.07.2016