Как сделать перечисление Swift со связанными значениями равными

У меня есть список связанных значений, которые я хотел бы сделать равными для тестирования, но я не знаю, как этот шаблон будет работать с случаем enum с более чем одним аргументом.

Например, кратко изложенный ниже, я знаю синтаксис для выравнивания заголовка. Как это будет работать для опций, которые содержат несколько значений разных типов?

enum ViewModel {
    case heading(String)
    case options(id: String, title: String, enabled: Bool)
}

func ==(lhs: ViewModel, rhs: ViewModel) -> Bool {
    switch (lhs, rhs) {
    case (let .heading(lhsString), let .heading(rhsString)):
        return lhsString == rhsString
    case options...
    default:
        return false
    }
}

Я знаю, что Swift 4.1 может синтезировать соответствие для Equatable для нас, но в настоящее время я не могу обновить эту версию.

6 ответов

Решение

Удобный способ - использовать кортежи по сравнению с ==, Многие также хотят включить код совместимости в проверку версии Swift, чтобы автоматический синтез использовался после обновления проекта до Swift 4.1:

enum ViewModel: Equatable {
    case heading(String)
    case options(id: String, title: String, enabled: Bool)

    #if swift(>=4.1)
    #else
    static func ==(lhs: ViewModel, rhs: ViewModel) -> Bool {
        switch (lhs, rhs) {
        case (let .heading(lhsString), let .heading(rhsString)):
            return lhsString == rhsString
        case (let .options(lhsId, lhsTitle, lhsEnabled), let .options(rhsId, rhsTitle, rhsEnabled)):
            return (lhsId, lhsTitle, lhsEnabled) == (rhsId, rhsTitle, rhsEnabled)
        default:
            return false
        }
    }
    #endif
}

Элегантный способ работы со связанным значением (даже если перечисление косвенное):

Для начала нужно иметь недвижимость:

      indirect enum MyEnum {
    var value: String? {
        return String(describing: self).components(separatedBy: "(").first
    }
    case greeting(text: String)
    case goodbye(bool: Bool)
    case hey
    case none
}

print(MyEnum.greeting(text: "Howdy").value)
// prints : greeting

теперь вы можете использовать value реализовать Equatable как это:

          indirect enum MyEnum: Equatable {
     static func == (lhs: MyEnum, rhs: MyEnum) -> Bool {
        lhs.value == rhs.value
     }
    
     var value: String? {
        return String(describing: self).components(separatedBy: "(").first
     }
     case greeting(text: String)
     case goodbye(bool: Bool)
     case hey
     case none
   }

Вы можете добавить что-то вроде ниже, проверьте эту ссылку для получения дополнительной информации. Заявление о возврате зависит от ваших потребностей.

#if swift(>=4.1)
#else
func ==(lhs: ViewModel, rhs: ViewModel) -> Bool {
    switch (lhs, rhs) {
    case (let .heading(lhsString), let .heading(rhsString)):
        return lhsString == rhsString

    case (let .options(id1, title1, enabled1),let .options(id2, title2, enabled2)):
        return id1 == id2 && title1 == title2 && enabled1 == enabled2
    default:
        return false
    }
}
#endif

Реализация Equatable для перечислений с неравными связанными типами

Краткое содержание

Для реализации перечисления с несвязанными значениями вы можете использовать его в качестве основы для проверки равенства. Это можно применить через удобный протокол (см. ниже).

Подробности:

Хотя, как упоминалось в некоторых других ответах, Swift может применяться к перечислениям со случаями, имеющими связанные значения, при условии, что эти значения сами по себе являются .

Например, в приведенном ниже примере, посколькууже соответствует , компилятор может синтезировать равенство для перечисления, просто украсив его протоколом...

      enum Response: Equatable {
    case success
    case failed(String)
}

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

      enum Response: Equatable {
    case success
    case failed(Error)
}

Еще больше разочаровывает то, что вы не можете вручную соответствовать протоколу «как есть», а не типу, и вы можете добавлять соответствие только к фактическим типам, а не к протоколам. Не зная, к каким фактическим типам применяется, вы не сможете проверить равенство.

Или есть?! ;)

Реализуйте равенство через

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

Эта возможность может быть реализована с возможностью повторного использования с помощью специального протокола, определенного как таковой...

      // Conform this protocol to Equatable
protocol ReflectiveEquatable: Equatable {}

extension ReflectiveEquatable {

    var reflectedValue: String { String(reflecting: self) }

    // Explicitly implement the required `==` function
    // (The compiler will synthesize `!=` for us implicitly)
    static func ==(lhs: Self, rhs: Self) -> Bool {
        return lhs.reflectedValue == rhs.reflectedValue
    }
}

Имея вышеизложенное, вы можете теперь соответствоватьenum to , тем самым передавая его неявно, и теперь он компилируется без проблем:

      // Make enum with non-`Equatable` associated values `Equatable`
enum Response: ReflectiveEquatable {
    case success
    case failed(Error)
}

Вы можете продемонстрировать, что он работает должным образом, с помощью следующего тестового кода:

      // Define custom errors (also with associated types)
enum MyError: Error {
    case primary
    case secondary
    case other(String)
}

enum MyOtherError: Error {
    case primary
}

// Direct check
print(Response.success == Response.success) // prints 'true'
print(Response.success != Response.success) // prints 'false'

// Same enum value, 'primary', but on different error types
// If we had instead used `String(describing:)` in the implementation,
// this would have matched giving us a false-positive.
print(Response.failed(MyError.primary) == Response.failed(MyError.primary)) // prints 'true'
print(Response.failed(MyError.primary) == Response.failed(MyOtherError.primary)) // prints 'false'

// Associated values of an enum which themselves also have associated values
print(Response.failed(MyError.other("A")) == Response.failed(MyError.other("A"))) // prints 'true'
print(Response.failed(MyError.other("A")) == Response.failed(MyError.other("B"))) // prints 'false'

Примечание. При соблюдении не создаются связанные типы!
(...но для того, чтобы это работало, они не обязательно должны быть такими!)

В приведенном выше примере важно отметить, что вы применяете протокол только к конкретному типу, к которому вы применяете протокол. Связанные типы, используемые внутри него, также не улавливают его.

Это означает, что в данном примеревсе еще не соответствует , поэтому приведенный ниже код все равно не сможет скомпилироваться...

      print(MyError.primary == MyError.primary) // Doesn't support equality so won't compile!

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

Например, это печатает отраженное значение случая перечисления, использованного в последнем тесте выше:

      print(Response.failed(MyError.other("A")).reflectedValue)

И вот результат:

      main.Response.failed(main.MyError.other("A"))

Примечание. «main» — это имя модуля, содержащего этот код.

Бонус:

Используя ту же технику, вы можете реализовать на основе отражения следующий протокол...

      // Conform this to both `Hashable` and `ReflectiveEquatable`
// (implicitly also conforming it to `Equatable`, a requirement of `Hashable`)
protocol ReflectiveHashable: Hashable, ReflectiveEquatable {}

// Implement the `hash` function.
extension ReflectiveHashable {
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(reflectedValue)
    }
}

При этом, если вы теперь согласуете свое перечисление с, ты получишь обаи(с помощью) легко и просто...

      // Make enum `Hashable` (and implicitly `Equatable`)
enum Response: ReflectiveHashable {
    case success
    case failed(Error)
}

Заключительные мысли – размышление? Действительно?!

Хотя следует признать, что отражение не является самым эффективным методом по сравнению со стандартными проверками на равенство (и при этом на несколько порядков), большинство людей неправильно задают вопрос: «Разве мы не должны избегать отражения, потому что это намного медленнее?» Реальный вопрос, который следует задать: «Действительно ли это должно быть быстрее?»

Подумайте, в какой области это может решить вашу проблему. Это происходит при обработке миллионов и миллионов проверок в секунду и когда производительность имеет решающее значение, или это, скорее, реакция на действия пользователя? Другими словами, замечаете ли вы, что он стал медленнее, или вы смотрите на это только с академической точки зрения?

Вывод: убедитесь, что вы не отказываетесь преждевременно от использования отражения, если оно решает для вас проблему, подобную описанной выше. Не оптимизируйте что-то, что на самом деле не меняет сути. Лучшее решение зачастую не самое быстрое в исполнении, а самое быстрое в завершении.

Возможно, не имеет отношения к OP, но это может помочь другим:

Помните, что если вы хотите сравнить значение перечисления только с фиксированным значением, вы можете просто использовать сопоставление с образцом:

if case let ViewModel.heading(title) = enumValueToCompare {
  // Do something with title
}

Если вас волнует связанное значение, вы можете добавить к нему некоторые условия:

if case let ViewModel.heading(title) = enumValueToCompare, title == "SomeTitle" {
  // Do something with title
}

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

      enum ViewModel: Equatable {
    case heading(String)
    case options(id: String, title: String, enabled: Bool)

    #if swift(>=4.1)
    #else
        static func == (lhs: ViewModel, rhs: ViewModel) -> Bool {
            switch (lhs, rhs) {
                case (.heading, .heading),
                     (.options, .options):
                    return true
                default:
                    return false
            }
        }
    #endif
}

let model1 = ViewModel.options(id: "1", title: "hello", enabled: true)
let model2 = ViewModel.options(id: "2", title: "hello", enabled: true)
let model3 = ViewModel.options(id: "1", title: "hello", enabled: true)

print(model1 == model2) // false
print(model1 == model3) // true
Другие вопросы по тегам