Can I define an enum as a subset of another enum&#

2019-04-25 09:26发布

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
}

2条回答
趁早两清
2楼-- · 2019-04-25 09:59

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
查看更多
做个烂人
3楼-- · 2019-04-25 10:03

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)
查看更多
登录 后发表回答