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

Posted

技术标签:

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

注意:这与我昨天在 *** 上发布的 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

【问题讨论】:

相关:Can an enum contain another enum values in Swift? 【参考方案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]

【讨论】:

以上是关于我可以将枚举定义为另一个枚举案例的子集吗?的主要内容,如果未能解决你的问题,请参考以下文章

将作用域枚举转换为 int

将一个枚举类型分配给另一个枚举类型

HDU 5616 Jam's balance(暴力枚举子集)

子集的生成—二进制枚举

是否可以在枚举关联值条件和另一个枚举案例之间编写复合开关案例?

枚举子集&高位前缀和