【问题标题】:Can I define an enum as a subset of another enum's cases?我可以将枚举定义为另一个枚举案例的子集吗?
【发布时间】:2017-03-31 23:46:45
【问题描述】:

注意:这与我昨天在 Stackoverflow 上发布的 another one 基本相同。但是,我认为我在那个问题中使用了一个糟糕的例子,它并没有完全归结为我所想的本质。由于对该原始帖子的所有回复都提到了第一个问题,我认为将新示例放在一个单独的问题中可能是一个更好的主意 - 无意重复。


模拟可以移动的游戏角色

让我们定义一个用于简单游戏的方向枚举:

enum Direction {
    case up
    case down
    case left
    case right
}

现在在游戏中我需要两种角色:

  • 一个只能左右移动的HorizontalMover
  • 一个只能上下移动的VerticalMover

它们都可以移动,所以它们都实现了

protocol Movable {
    func move(direction: Direction)
}

那么让我们定义两个结构体:

struct HorizontalMover: Movable {
    func move(direction: Direction)
    let allowedDirections: [Direction] = [.left, .right]
}

struct VerticalMover: Movable {
    func move(direction: Direction)
    let allowedDirections: [Direction] = [.up, .down]
}

问题

...使用这种方法,我仍然可以将不允许的值传递给move() 函数,例如以下调用将是有效的:

let horizontalMover = HorizontalMover()
horizontalMover.move(up) // ⚡️

当然,我可以在 move() 函数内部检查此 Mover 类型是否允许传递的 direction,否则抛出错误。但是因为我确实知道在编译时允许哪些情况我也希望在编译时进行检查

所以我真正想要的是:

struct HorizontalMover: Movable {
    func move(direction: HorizontalDirection)
}

struct VerticalMover: Movable {
    func move(direction: VerticalDirection)
}

其中HorizontalDirectionVerticalDirectionDirection 枚举的子集枚举。

像这样独立定义两个方向类型没有多大意义,没有任何共同的“祖先”:

enum HorizontalDirection {
    case left
    case right
}

enum VerticalDirection {
    case up
    case down
}

因为我必须一遍又一遍地重新定义相同的情况,这些情况对于每个表示方向的枚举在语义上是相同的。例如。如果我添加另一个可以向任何方向移动的角色,我还必须实现大方向枚举(如上所示)。然后我在HorizontalDirection 枚举中有一个left 案例,在一般Direction 枚举中有一个left 案例,它们彼此不了解,这不仅丑陋,而且在分配和分配时成为一个真正的问题利用我必须在每次枚举中重新分配的原始值。


那么有没有办法解决这个问题?

我可以像这样将一个枚举定义为另一个枚举的案例的子集吗?

enum HorizontalDirection: Direction {
    allowedCases:
        .left
        .right
}

【问题讨论】:

标签: swift enums subset restriction


【解决方案1】:

没有。 Swift 枚举目前无法做到这一点。

我能想到的解决方案:

  • 使用我在您的其他问题中概述的协议
  • 回退到运行时检查

【讨论】:

    【解决方案2】:

    这是一个可能的编译时解决方案:

    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"
    }
    

    现在我们可以像这样定义一个move 方法:

    func move(_ allowedDirection: HorizontalDirection) {
      let direction = allowedDirection.rawValue
      print(direction)
    }
    

    这种方法的缺点是您需要确保各个枚举中的字符串是正确的,这可能容易出错。出于这个原因,我特意使用了ExpressibleByStringLiteral,而不是ExpressibleByIntegerLiteral,因为我认为它更具可读性和可维护性——你可能不同意。

    您还需要定义所有 3 个初始化器,这可能有点笨拙,但如果您使用 ExpressibleByIntegerLiteral 代替,您可以避免这种情况。

    我知道您正在用编译时安全性在一个地方换另一个地方,但我想这种解决方案在某些情况下可能更可取。

    为确保您没有任何错误输入的字符串,您还可以添加一个简单的单元测试,如下所示:

    XCTAssertEqual(Direction.left, HorizontalDirection.left.rawValue)
    XCTAssertEqual(Direction.right, HorizontalDirection.right.rawValue)
    XCTAssertEqual(Direction.up, VerticalDirection.up.rawValue)
    XCTAssertEqual(Direction.down, VerticalDirection.down.rawValue)
    

    【讨论】:

      【解决方案3】:

      您可能已经解决了您的问题,但对于任何正在寻找答案的人来说,一段时间以来(不确定 Apple 何时引入它),您可以在枚举案例中使用关联值来模拟这些类型的状态。

      enum VerticalDirection {
          case up
          case down
      }
      
      enum HorizontalDirection {
          case left
          case right
      }
      
      enum Direction {
          case vertical(direction: VerticalDirection)
          case horizontal(direction: HorizontalDirection)
      }
      

      所以你可以使用这样的方法:

      func move(_ direction: Direction) {
          print(direction)
      }
      
      move(.horizontal(.left))
      

      如果你遵守 Equatable 协议:

      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
              }
          }
      }
      

      你可以这样做:

      func isMovingLeft(direction: Direction) -> Bool {
          return direction == .horizontal(.left)
      }
      
      let characterDirection: Direction = .horizontal(.left)
      isMovingLeft(direction: characterDirection) // true
      isMovingLeft(direction: characterDirection) // false
      

      【讨论】:

        【解决方案4】:

        使用 Swift 协议选项集

        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]
        }
        

        【讨论】:

          猜你喜欢
          • 2017-03-30
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2017-04-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多