Note: This is basically the same question as another one I've posted on Stackoverflow yesterday. However, I figured that I used a poor example in that question that didn't quite boil it down to the essence of what I had in mind. As all replies to that original post refer to that first question I thought it might be a better idea to put the new example in a separate question — no duplication intended.
Let's define an enum of directions for use in a simple game:
enum Direction {
case up
case down
case left
case right
}
Now in the game I need two kinds of characters:
HorizontalMover
that can only move left and right.VerticalMover
that can only move up and down.They can both move so they both implement the
protocol Movable {
func move(direction: Direction)
}
So let's define the two structs:
struct HorizontalMover: Movable {
func move(direction: Direction)
let allowedDirections: [Direction] = [.left, .right]
}
struct VerticalMover: Movable {
func move(direction: Direction)
let allowedDirections: [Direction] = [.up, .down]
}
... with this approach is that I can still pass disallowed values to the move()
function, e.g. the following call would be valid:
let horizontalMover = HorizontalMover()
horizontalMover.move(up) // ⚡️
Of course I can check inside the move()
funtion whether the passed direction
is allowed for this Mover type and throw an error otherwise. But as I do have the information which cases are allowed at compile time I also want the check to happen at compile time.
So what I really want is this:
struct HorizontalMover: Movable {
func move(direction: HorizontalDirection)
}
struct VerticalMover: Movable {
func move(direction: VerticalDirection)
}
where HorizontalDirection
and VerticalDirection
are subset-enums of the Direction
enum.
It doesn't make much sense to just define the two direction types independently like this, without any common "ancestor":
enum HorizontalDirection {
case left
case right
}
enum VerticalDirection {
case up
case down
}
because then I'd have to redefine the same cases over and over again which are semantically the same for each enum that represents directions. E.g. if I add another character that can move in any direction, I'd have to implement the general direction enum as well (as shown above). Then I'd have a left
case in the HorizontalDirection
enum and a left
case in the general Direction
enum that don't know about each other which is not only ugly but becomes a real problem when assigning and making use of raw values that I would have to reassign in each enumeration.
Can I define an enum as a subset of the cases of another enum like this?
enum HorizontalDirection: Direction {
allowedCases:
.left
.right
}
No. This is currently not possible with Swift enums.
The solutions I can think of:
Here's a possible compile-time solution:
enum Direction: ExpressibleByStringLiteral {
case unknown
case left
case right
case up
case down
public init(stringLiteral value: String) {
switch value {
case "left": self = .left
case "right": self = .right
case "up": self = .up
case "down": self = .down
default: self = .unknown
}
}
public init(extendedGraphemeClusterLiteral value: String) {
self.init(stringLiteral: value)
}
public init(unicodeScalarLiteral value: String) {
self.init(stringLiteral: value)
}
}
enum HorizontalDirection: Direction {
case left = "left"
case right = "right"
}
enum VerticalDirection: Direction {
case up = "up"
case down = "down"
}
Now we can define a move
method like this:
func move(_ allowedDirection: HorizontalDirection) {
let direction = allowedDirection.rawValue
print(direction)
}
The drawback of this approach is that you need to make sure that the strings in your individual enums are correct, which is potentially error-prone. I have intentionally used ExpressibleByStringLiteral
for this reason, rather than ExpressibleByIntegerLiteral
because it is more readable and maintainable in my opinion - you may disagree.
You also need to define all 3 of those initializers, which is perhaps a bit unwieldy, but you would avoid that if you used ExpressibleByIntegerLiteral
instead.
I'm aware that you're trading compile-time safety in one place for another, but I suppose this kind of solution might be preferable in some situations.
To make sure that you don't have any mistyped strings, you could also add a simple unit test, like this:
XCTAssertEqual(Direction.left, HorizontalDirection.left.rawValue)
XCTAssertEqual(Direction.right, HorizontalDirection.right.rawValue)
XCTAssertEqual(Direction.up, VerticalDirection.up.rawValue)
XCTAssertEqual(Direction.down, VerticalDirection.down.rawValue)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With