【问题标题】:Can I restrict an enum to certain cases of another enum?我可以将一个枚举限制为另一个枚举的某些情况吗?
【发布时间】:2017-03-30 21:46:40
【问题描述】:

假设我有一家面包店和一份原料清单:

enum Ingredient {
    case flower     = 1
    case sugar      = 2
    case yeast      = 3
    case eggs       = 4
    case milk       = 5
    case almonds    = 6
    case chocolate  = 7
    case salt       = 8
}

一个案例的rawValue代表库存编号。

然后我有两个食谱:

巧克力蛋糕:

  • 500g花
  • 300克糖
  • 3 个鸡蛋
  • 200毫升牛奶
  • 200克巧克力

杏仁蛋糕:

  • 300克花
  • 200克糖
  • 20g酵母
  • 200克杏仁
  • 5 个鸡蛋
  • 2g 盐

现在我定义一个函数

func bake(with ingredients: [Ingredient]) -> Cake

我当然信任我的员工,但我仍然想确保他们只使用正确的原料来烘烤蛋糕。 ????

我可以通过像这样定义两个单独的枚举来做到这一点:

enum ChocolateCakeIngredient {
    case flower
    case sugar
    case eggs
    case milk
    case chocolate
}

enum AlmondCakeIngredient {
    case flower
    case sugar
    case yeast
    case eggs
    case almonds
    case salt
}

然后像这样烤蛋糕:

// in chocolate cake class / struct:
func bake(with ingredients: [ChocolateCakeIngredient]) -> ChocolateCake
// in almond cake class / struct:
func bake(with ingredients: [AlmondCakeIngredient]) -> AlmondCake

但是我不得不一遍又一遍地重新定义相同的成分,因为两种蛋糕都使用了许多成分。我真的不想那样做——尤其是在枚举案例中附加了库存编号为rawValues。

这让我想到了一个问题,在 Swift 中是否有办法将一个枚举限制为另一个枚举的某些情况?类似(伪代码):

enum ChocolateCakeIngredient: Ingredient {
    allowedCases:
        case flower
        case sugar
        case eggs
        case milk
        case chocolate
}

enum AlmondCakeIngredient: Ingredient {
    allowedCases:
        case flower
        case sugar
        case yeast
        case eggs
        case almonds
        case salt
}

这样的组合可能吗?我该怎么做?

或者我可以在这种情况下使用另一种模式?


更新

从这个问题的所有 cmets 和答案中,我认为我为这个问题选择的示例有点不合适,因为它没有归结问题的本质,并且在类型安全方面留下了漏洞。

由于此页面上的所有帖子都与此特定示例相关,因此我在 Stackoverflow 上创建了一个新问题,其中包含一个更易于理解且一针见血的示例:

➡️Same question with a more specific example

【问题讨论】:

  • 在 Java 中,这在编译时更难实现,而在运行时更容易实现。您的员工会自己编写代码吗?如果不是,则没有强制要求为您的子类型提供严格的类型安全。在运行时对代码中的集合进行错误检查会更简单。
  • 您似乎正试图将约束放在枚举中,而不是将它们保留在它们所属的位置 - 使用枚举的类型。这看起来不像是可扩展的架构。
  • 您可以通过将每个case 设为结构然后将协议AlmondCakeIngredient 添加到可用于制作杏仁蛋糕的每种成分来做到这一点,但您会遇到其他可扩展性问题。
  • 另外,添加新成分需要重新编译代码。例如,为什么不将成分作为数据保存在外部文件中?
  • @scottb:我的“员工”只是留在面包店样本域中的一个隐喻。我现实我将编写代码或我的一些队友。这只是说“我想要类型安全(在编译时)”。 ;) 你是对的:在运行时进行检查似乎很多更容易,不仅在 Java 中,在 Swift 中也是如此。

标签: swift enums restriction


【解决方案1】:

我认为不可能在编译时执行这样的检查。这是在运行时构建代码以执行此操作的一种方法:

enum Ingredient: Int {
  case flour = 1
  case sugar = 2
  case yeast = 3
  case eggs = 4
  case milk = 5
  case almonds = 6
  case chocolate = 7
  case salt = 8
}

protocol Cake {
  init()
  static var validIngredients: [Ingredient] { get }
}

extension Cake {
  static func areIngredientsAllowed(_ ingredients: [Ingredient]) -> Bool {
    for ingredient in ingredients {
      if !validIngredients.contains(ingredient) {
        return false
      }
    }
    return true
  }
}

class ChocolateCake: Cake {
  required init() {}
  static var validIngredients: [Ingredient] = [.flour, .sugar, .eggs, .milk, .chocolate]
}

class AlmondCake: Cake {
  required init() {}
  static var validIngredients: [Ingredient] = [.flour, .sugar, .yeast, .eggs, .almonds, .salt]
}

bake 方法如下所示:

func bake<C: Cake>(ingredients: [Ingredient]) -> C {

  guard C.areIngredientsAllowed(ingredients) else {
    fatalError()
  }

  let cake = C()
  // TODO: Let's bake!
  return cake
}

现在我可以说:

let almondCake: AlmondCake = bake(ingredients: ingredients)

...并确保只使用有效成分。

【讨论】:

  • 这是一个非常简洁的实现,如果绝对没有办法在编译时使用enums 执行检查,我可能最终会使用类似的实现。谢谢! (我实际上是在寻找编译时解决方案,但没有明确说明。)我建议对性能进行一点改进:如果您枚举成分而不是使用 reduce 函数,则可以将类型检查中断为一旦发现第一个不允许的成分。
  • 确实如此。正如在别处提到的那样,有效成分可能会更好作为一个集合。
  • 嗨@ganzogo,根据您上面的解决方案,Ingredient 枚举如何仅对采用Cake 协议的人可用?目前,其他文件可以访问Ingredient枚举。
【解决方案2】:

另一种方法:使用选项集类型

或者我可以在这种情况下使用另一种模式?

另一种方法是让您的Ingredient 成为OptionSet 类型(符合协议OptionsSet 的类型):

例如

struct Ingredients: OptionSet {
    let rawValue: UInt8

    static let flower    = Ingredients(rawValue: 1 << 0) //0b00000001
    static let sugar     = Ingredients(rawValue: 1 << 1) //0b00000010
    static let yeast     = Ingredients(rawValue: 1 << 2) //0b00000100
    static let eggs      = Ingredients(rawValue: 1 << 3) //0b00001000
    static let milk      = Ingredients(rawValue: 1 << 4) //0b00010000
    static let almonds   = Ingredients(rawValue: 1 << 5) //0b00100000
    static let chocolate = Ingredients(rawValue: 1 << 6) //0b01000000
    static let salt      = Ingredients(rawValue: 1 << 7) //0b10000000

    // some given ingredient sets
    static let chocolateCakeIngredients: Ingredients = 
        [.flower, .sugar, .eggs, .milk, .chocolate]
    static let almondCakeIngredients: Ingredients = 
        [.flower, .sugar, .yeast, .eggs, .almonds, .salt]
}

应用于您的bake(with:) 示例,其中员工/开发人员尝试在bake(with:) 的主体中实现巧克力蛋糕的烘焙:

/* dummy cake */
struct Cake {
    var ingredients: Ingredients
    init(_ ingredients: Ingredients) { self.ingredients = ingredients }
}

func bake(with ingredients: Ingredients) -> Cake? {
    // lets (attempt to) bake a chokolate cake
    let chocolateCakeWithIngredients: Ingredients = 
        [.flower, .sugar, .yeast, .milk, .chocolate]
                        // ^^^^^ ups, employee misplaced .eggs for .yeast!

    /* alternatively, add ingredients one at a time / subset at a time
    var chocolateCakeWithIngredients: Ingredients = []
    chocolateCakeWithIngredients.formUnion(.yeast) // ups, employee misplaced .eggs for .yeast!
    chocolateCakeWithIngredients.formUnion([.flower, .sugar, .milk, .chocolate]) */

    /* runtime check that ingredients are valid */
    /* ---------------------------------------- */

    // one alternative, invalidate the cake baking by nil return if 
    // invalid ingredients are used
    guard ingredients.contains(chocolateCakeWithIngredients) else { return nil }
    return Cake(chocolateCakeWithIngredients)

    /* ... or remove invalid ingredients prior to baking the cake 
    return Cake(chocolateCakeWithIngredients.intersection(ingredients)) */

    /* ... or, make bake(with:) a throwing function, which throws and error
       case containing the set of invalid ingredients for some given attempted baking */
}

使用给定的可用巧克力蛋糕原料致电bake(with:)

if let cake = bake(with: Ingredients.chocolateCakeIngredients) {
    print("We baked a chocolate cake!")
}
else {
    print("Invalid ingredients used for the chocolate cake ...")
} // Invalid ingredients used for the chocolate cake ...

【讨论】:

    【解决方案3】:

    静态解决方案:

    如果配方数量始终相同,则可以在枚举中使用函数:

        enum Ingredient {
            case chocolate
            case almond
    
            func bake() -> Cake {
                switch self {
                case chocolate:
                    print("chocolate")
                    /*
                     return a Chocolate Cake based on:
    
                     500g flower
                     300g sugar
                     3 eggs
                     200ml milk
                     200g chocolate
                     */
                case almond:
                    print("almond")
                    /*
                     return an Almond Cake based on:
    
                     300g flower
                     200g sugar
                     20g yeast
                     200g almonds
                     5 eggs
                     2g salt
                     */
                }
            }
        }
    

    用法:

    // bake chocolate cake
    let bakedChocolateCake = Ingredient.chocolate.bake()
    
    // bake a almond cake
    let bakedAlmondCake = Ingredient.almond.bake()
    

    动态解决方案:

    如果配方数量是可变的——这就是我的假设——我通过使用一个单独的 model 类有点作弊:)

    如下:

    class Recipe {
        private var flower = 0
        private var sugar = 0
        private var yeast = 0
        private var eggs = 0
        private var milk = 0
        private var almonds = 0
        private var chocolate = 0
        private var salt = 0
    
        // init for creating a chocolate cake:
        init(flower: Int, sugar: Int, eggs: Int, milk: Int, chocolate: Int) {
            self.flower = flower
            self.sugar = sugar
            self.eggs = eggs
            self.milk = milk
            self.chocolate = chocolate
        }
    
        // init for creating an almond cake:
        init(flower: Int, sugar: Int, yeast: Int, almonds: Int, eggs: Int, salt: Int) {
            self.flower = flower
            self.sugar = sugar
            self.yeast = yeast
            self.almonds = almonds
            self.eggs = eggs
            self.salt = salt
        }
    }
    
    enum Ingredient {
        case chocolate
        case almond
    
        func bake(recipe: Recipe) -> Cake? {
            switch self {
            case chocolate:
                print("chocolate")
                if recipe.yeast > 0 || recipe.almonds > 0 || recipe.salt > 0 {
                    return nil
                    // or maybe a fatal error!!
                }
    
                // return a Chocolate Cake based on the given recipe:
            case almond:
                print("almond")
                if recipe.chocolate > 0 {
                    return nil
                    // or maybe a fatal error!!
                }
    
                // return an Almond Cake based on the given recipe:
            }
        }
    }
    

    用法:

    // bake chocolate cake with a custom recipe
    let bakedChocolateCake = Ingredient.chocolate.bake(Recipe(flower: 500, sugar: 300, eggs: 3, milk: 200, chocolate: 200)
    
    // bake almond cake with a custom recipe
    let bakedAlmondCake = Ingredient.chocolate.bake(Recipe(flower: 300, sugar: 200, yeast: 20, almonds: 200, eggs: 5, salt: 2))
    

    即使这些不是您的案例的最佳解决方案,我希望它有所帮助。

    【讨论】:

    • 感谢您的回答。这段代码可能适用于我的问题中概述的特定情况,但在我看来,从架构的角度来看它没有多大意义:你正在烤蛋糕,而不是一种配料,那么你为什么要打电话给bake(aReceipe)特殊成分?此外,如果我稍后添加成分,我将不得不检查每一个蛋糕并检查新成分 → 不可扩展。
    • @Mischa 你是对的......不知何故,我能感觉到一种糟糕的“代码味道”。谢天谢地,我还没有投反对票 :) 我会保留它,它可能会提供一个想法......你认为我应该删除它吗?
    【解决方案4】:

    你可以在 Swift 中做这样的事情:

    enum Ingredients {
        struct Flower { }
        struct Sugar { }
        struct Yeast { }
        struct Eggs { }
        struct Milc { }
    }
    
    protocol ChocolateCakeIngredient { }
    extension Sugar: ChocolateCakeIngredient { }
    extension Eggs: ChocolateCakeIngredient { }
    ...
    
    func bake(ingredients: [ChocolateCakeIngredient]) { }
    

    在这个例子中,我使用枚举 Ingredients 作为我所有成分的命名空间。这也有助于代码完成。

    然后,为每个配方创建一个协议,并使该配方中的成分符合该协议。

    虽然这应该可以解决您的问题,但我不确定您是否应该这样做。这(以及您的伪代码)将强制在烘烤巧克力蛋糕时没有人可以传递不属于巧克力蛋糕的成分。但是,它不会禁止任何人尝试使用空数组或类似的东西调用bake(with ingredients:)。因此,您实际上不会通过您的设计获得任何安全性。

    【讨论】:

    • 此外,枚举的行为不再像枚举。您还需要一个由每种成分实现的协议Ingredient 和一个用于比较它们的标识符。
    • 确实如此。正如我在帖子末尾所说的那样,我认为尽管它解决了问题,但您可能不应该这样做。所有这些实现都不能保证bake(with:) 的调用者在编译时传递了完全正确的参数——这使得它变得毫无意义。
    • 我选择的例子很糟糕。我从我的实际问题中做了一点抽象,目的是让这个例子更容易理解,但我确实错过了这个(非常明显的)漏洞。在我面临的实际问题中,我只传递了enum 类型的一个参数,而不是数组。在那种情况下就不会有这样的漏洞——我总是必须通过一个特定的枚举案例。
    • 我同意@Sulthan:这个实现的问题是我没有得到正常的enum 行为,这在这里完全有意义。此外,这感觉有点像滥用 Swift 构造来达到它们不打算用于的目的。至少,代码对我来说有点不直观。它没有表现出ChocolateCakeIngredient 实际上是Ingredient
    • 那是因为它不需要。在上面的代码中,完全有可能以非成分的类型实现ChocolateCakeIngredient。方法类型约束是“实现ChocolateCakeIngredient的东西”。
    【解决方案5】:

    我认为您应该将特定食谱的成分列为:

    let chocolateCakeIngredients: [Ingredient] = [.flower, ...]
    

    然后只需检查该列表是否包含所需的成分。

    【讨论】:

    • 这可能会更好地实现为Set,用于恒定时间查找。
    • 这当然是一个可行的解决方案,@ganzogo 在他的回答中很好地实现了它。我同意使用Set 而不是Array 甚至可以提高大量Ingredients 的性能。但是(我忘了在我的问题中明确提到这一点),这种方法并没有给我编译时安全性,这是我在编写 时所想到的“我仍然想确保他们只使用正确的烤蛋糕的原料”。事实上,允许的Ingredients(案例)在编译时就已经知道了。
    猜你喜欢
    • 2017-03-31
    • 1970-01-01
    • 2017-04-01
    • 1970-01-01
    • 1970-01-01
    • 2010-12-22
    • 1970-01-01
    • 1970-01-01
    • 2019-01-29
    相关资源
    最近更新 更多