I have an enum of associated values which I would like to make equatable for testing purposes, but do not know how this pattern would work with an enum case with more than one argument.
For example, summarised below I know the syntax for making heading equatable. How would this work for options, which contains multiple values of different types?
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
}
}
I know Swift 4.1 can synthesize conformance for Equatable for us, but at present I am not able to update to this version.
SE-0185 Synthesizing Equatable and Hashable conformance has been implemented in Swift 4.1, so that it suffices do declare conformance to the protocol (if all members are Equatable
):
enum ViewModel: Equatable {
case heading(String)
case options(id: String, title: String, enabled: Bool)
}
For earlier Swift versions, a convenient way is to use that tuples can be compared with ==
.
You many also want to enclose the compatibility code in a Swift version check, so that the automatic synthesis is used once the project is updated to 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
}
You can add something like below, check this link for more information. Return statement for options depend on your needs.
#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
Maybe not relevant for the OP but this might help others:
Remember that if you only want to compare an enum value against a fixed value , you can simply use pattern matching:
if case let ViewModel.heading(title) = enumValueToCompare {
// Do something with title
}
If you care about the associated value, you can add some conditions on it:
if case let ViewModel.heading(title) = enumValueToCompare, title == "SomeTitle" {
// Do something with title
}
To implement Equatable
on an enumeration with non- Equatable
associated values, you can leverage String(reflecting:)
as the basis for checking equality. This can be applied via a convenience ReflectiveEquatable
protocol (see below.)
While as mentioned in some of the other answers, Swift can apply Equatable
to enums with cases having associated values provided those values are themselves Equatable
.
For instance, in the below example, since String
already conforms to Equatable
, the compiler can synthesize equality for the Response
enum by simply decorating it with the Equatable
protocol...
enum Response: Equatable {
case success
case failed(String)
}
However, this one will not compile because the associated type Error
is not itself Equatable
, thus the compiler can't synthesize equality for us...
enum Response: Equatable {
case success
case failed(Error)
}
More frustratingly, you can't manually conform Error
to Equatable
as Error
is a protocol, not a type, and you can only add conformance to actual types, not protocols. Without knowing the actual types Error
is applied to, there's no way for you to check for equality.
Or is there?; ;)
String(reflecting:)
The solution I propose is to leverage String(reflecting:)
to implement equality. Reflection works recursively through all nested and associated types, resulting in a unique string that we ultimately use for the equality check.
This capability can be implemented in a reusable fashion via a custom ReflectiveEquatable
protocol, defined as such...
// 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
}
}
With the above in place, you can now conform the Response
enum to ReflectiveEquatable
, thus giving it Equatable
implicitly, and it now compiles without issue:
// Make enum with non-`Equatable` associated values `Equatable`
enum Response: ReflectiveEquatable {
case success
case failed(Error)
}
You can demonstrate it's working as expected with the following test code:
// 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'
Note: Adhering to ReflectiveEquatable
does not make the associated types Equatable
!
(...but they also don't have to be for this to work!)
In the above example, it's important to note you are only applying Equatable
to the specific type you apply the ReflectiveEquatable
protocol to. The associated types used within it do not pick it up as well.
This means in this example, Error
still doesn't conform to Equatable
, thus the code below will still fail to compile...
print(MyError.primary == MyError.primary) // Doesn't support equality so won't compile!
The reason this implementation still works is because as mentioned above, we're not relying on the associated values' conformance to Equatable
. We're instead relying on how the specific enum's case appears under reflection, and since that results in a string which takes all associated values into consideration (by recursively reflecting on them as well), we get a pretty unique string, and that's what's ultimately checked.
For instance, this prints the reflected value of the enum case used in the last test above:
print(Response.failed(MyError.other("A")).reflectedValue)
And here's the resulting output:
main.Response.failed(main.MyError.other("A"))
Note: 'main' here is the module name containing this code.
ReflectiveHashable
Using the same technique, you can implement Hashable
based on reflection with the following ReflectiveHashable
protocol...
// 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)
}
}
With that in place, if you now conform your enum to ReflectiveHashable
, you get both Hashable
and Equatable
(via ReflectiveEquatable
) simply and easily...
// Make enum `Hashable` (and implicitly `Equatable`)
enum Response: ReflectiveHashable {
case success
case failed(Error)
}
Final Thoughts - Reflection? Really?!
While admittedly, reflection is not the most performant method compared to standard equality checks (and by several orders of magnitude at that), the question most people incorrectly ask is 'Shouldn't we avoid reflecting because it's so much slower?' The real question one should ask is 'Does it actually need to be faster?'
Consider the area where this may solve a problem for you. Is it while processing millions and millions of checks a second and where performance is critical, or is it more likely in response to a user-action? In other words, do you even notice that it's slower, or are you only looking at it from an academic standpoint?
The takeaway here being make sure you're not prematurely discounting the use of reflection if it solves a problem for you like the above. Don't optimize for something that doesn't actually move the needle. The best solution often isn't the fastest to run, but the fastest to be finished.
Elegant way to work with associated value ( even if the enum is indirect):
first you need to have the value
property:
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
now you can use the value
to implement Equatable
like this:
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
}
In the original solution, there is a section of code that can be simplified if you want to compare only the enum cases without comparing their associated values, Here's the updated code:
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
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.