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.
Model Game Characters That Can Move
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:
- A
HorizontalMover
that can only move left and right.
← → - A
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]
}
The Problem
... 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.
So is there a way out of this?
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:
- Use protocols as I outlined in your other question
- Fallback to a runtime check
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)
You probably solved your issue, but to anyone looking for an answer, for some time now (not sure when Apple introduced it) you can use associated values inside enum cases to model these kinds of states.
enum VerticalDirection {
case up
case down
}
enum HorizontalDirection {
case left
case right
}
enum Direction {
case vertical(direction: VerticalDirection)
case horizontal(direction: HorizontalDirection)
}
So you can use a method like this:
func move(_ direction: Direction) {
print(direction)
}
move(.horizontal(.left))
And if you conform to Equatable protocol:
extension Direction: Equatable {
static func ==(lhs: Direction, rhs: Direction) -> Bool {
switch (lhs, rhs) {
case (.vertical(let lVertical), .vertical(let rVertical)):
switch (lVertical, rVertical) {
case (.up, .up):
return true
case (.down, .down):
return true
default:
return false
}
case (.horizontal(let lHorizontal), .horizontal(let rHorizontal)):
switch (lHorizontal, rHorizontal) {
case (.left, .left):
return true
case (.right, .right):
return true
default:
return false
}
default:
return false
}
}
}
you can do something like this:
func isMovingLeft(direction: Direction) -> Bool {
return direction == .horizontal(.left)
}
let characterDirection: Direction = .horizontal(.left)
isMovingLeft(direction: characterDirection) // true
isMovingLeft(direction: characterDirection) // false
Use Swift protocol OptionSet
struct Direction: OptionSet {
let rawValue: int
static let up = Direction(rawValue: 1<<0)
static let right = Direction(rawValue: 1<<1)
static let down = Direction(rawValue: 1<<2)
static let left = Direction(rawValue: 1<<3)
static let horizontal = [.left, .right]
static let vertical = [.up, down]
}
来源:https://stackoverflow.com/questions/40654823/can-i-define-an-enum-as-a-subset-of-another-enums-cases