我可以将枚举限制为另一个枚举的某些情况吗?

时间:2016-11-16 15:53:22

标签: swift enums restriction

说我有面包店和成分清单:

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克巧克力

杏仁饼:

  • 300g花
  • 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

但随后我不得不一遍又一遍地重新定义相同的成分,因为两种蛋糕都使用了许多成分。我真的不想这样做 - 特别是因为枚举案例附加了rawValue的库存号。

这引出了一个问题,如果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
}

这样的作品可能吗?我该怎么办?

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

更新

从这个问题的所有评论和答案中,我认为我为这个问题选择的例子有点不合适,因为它没有归结为问题的本质,并留下了关于类型安全的漏洞。

由于此页面上的所有帖子都与此特定示例相关,因此我在Stackoverflow上创建了一个新问题,其中一个示例更易于理解,并且可以直接触及:

➡️Same question with a more specific example

5 个答案:

答案 0 :(得分:2)

我认为你应该列出具体食谱的成分:

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

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

答案 1 :(得分: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)

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

答案 2 :(得分:1)

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

Ingredients

在这个例子中,我使用enum bake(with ingredients:)作为我所有ingedients的命名空间。这也有助于代码完成。

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

虽然这可以解决你的问题,但我不确定你应该这样做。这个(以及你的伪代码)将强制执行当烘焙时没有人可以传递不属于巧克力蛋糕的成分。但是,它不会禁止任何人尝试使用空数组或类似的东西来调用oac.bat 09:09 klm.txt 9:00 。因此,您的设计实际上不会获得任何安全性。

答案 3 :(得分:1)

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

  

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

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

E.g。

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:)示例,其中employee / dev尝试在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 ...

答案 4 :(得分:0)

静态解决方案:

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

    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()

动态解决方案:

如果配方数量是可变的 - 这就是我的假设 - 我通过使用分离的模型类来欺骗了一点:)

如下:

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

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