Как сделать перечисление 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
}
}
Имея вышеизложенное, вы можете теперь соответствовать
// 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