【问题标题】:Is a switch statement appropriate here, taking an enum?switch 语句在这里是否合适,采用枚举?
【发布时间】:2019-08-21 06:38:15
【问题描述】:

今天我用 Java 制作了一个俄罗斯方块克隆,到了实现块生成机制的时候,我写了这个带有枚举的 switch 语句。有人告诉我尽可能避免使用 switch 语句,但我不确定是否可以在此处避免使用 switch 语句,除非我彻底检查我最初的基于继承的设计选择。

为了实现不同类型的块,我决定创建一个名为 Block 的抽象超类,并为每个特定类型创建一个子类。我还制作了一个标记块类型的枚举。例如,ZBlock extends Block 对象会将 Type.ZBlock 枚举分配给其 Type 字段变量。

private void spawnBlock(Type type){
        switch(type){
            case I:
                currentBlock = new IBlock();
                break;
            case L:
                currentBlock = new LBlock();
                break;
            case J:
                currentBlock = new JBlock();
                break;
            case Z:
                currentBlock = new ZBlock();
                break;
            case S:
                currentBlock = new SBlock();
                break;
            case T:
                currentBlock = new TBlock();
                break;
            default:
                currentBlock = new OBlock();
        }
    }

当我开始考虑在此处不使用 switch 语句的方法时,除了摆脱所有子类并在单个非抽象 Block 类中对不同块的行为进行编程之外,我想不出别的办法.但它可能会变得一团糟,因为不同的块有不同的形状和墙踢数据。那么 switch 语句在这里是一个不错的选择吗?如果没有,我该如何改进我的代码?为什么?

【问题讨论】:

  • “我被告知要尽可能避免使用 switch 语句”。这根本不是真的。
  • @Sweeper 本来可以告诉他的,但是告诉他的人并没有告诉他好东西。
  • OP,switch 可以
  • 为什么不合适?我看到的直接替代方案是很多 if else if,这只会创建混乱的代码。只是不要忘记:switch 不会做空检查
  • “尽可能避免 switch 语句” 无疑是“……为愚者的服从和智者的指导”的规则之一。你不应该不惜一切代价避免它们。但是,在许多的情况下,switch-statements(甚至经常是if-statements)在引用任何类型的type时都应该仔细查看。 i>,如switch(type)if(type==X)。这当然是一个危险信号,应该仔细考虑是否不能更恰当地用多态来解决(这里基本上也是这种情况)。

标签: java oop switch-statement


【解决方案1】:

Switch 比使用 if-else 语句更好。在我看来,这是更简洁的代码。 只需使用 if-else 将其与相同的代码进行比较:

private void spawnBlock(Type type){
    if(Type.I.equals(type)) {
        currentBlock = new IBlock();
    } else if(Type.L.equals(type)) {
        currentBlock = new LBlock();
    } else if(Type.J.equals(type)) {
        currentBlock = new JBlock();
    } else if(Type.Z.equals(type)) {
        currentBlock = new ZBlock();
    } else if(Type.S.equals(type)) {
        currentBlock = new SBlock();
    } else if(Type.T.equals(type)) {
        currentBlock = new TBlock();
    } else {
        currentBlock = new OBlock();
    }
}

但您通常有机会使用与 switch 或 if-else 不同的方法。 只需看看其他答案或查看this。它解释了如何使用枚举改进代码并将逻辑直接放入枚举中。

【讨论】:

    【解决方案2】:

    还有另一种避免switch 情况的方法,即使用MapEnunMap 实现,它将Type 作为键,将值作为Block 实现。

    在处理 Enum 键时,建议按照 Javadoc 实现 EnumMap

    用于枚举类型键的专用 Map 实现。

    这种表示非常紧凑和高效。

    实现说明:所有基本操作都在恒定时间内执行。他们很可能 (虽然不能保证)比它们的 HashMap 对应物更快。

    使用EnumMap 代替switch 时的示例代码,其中Type 枚举实例,值是Supplier<Block>(构造函数引用):

    //During initialization
    Map<Type, Supplier<Block>> registry =  new EnumMap<>(Type.class);
    registry.put(Type.I, IBlock::new);
    registry.put(Type.L, LBlock::new);
    registry.put(Type.J, JBlock::new);
    registry.put(Type.Z, ZBlock::new);
    ...
    

    然后您可以通过提供正确的TypespawnBlock() 获取它,并且由于最后调用Supplier.get(),您每次都会获得Block 实现的新实例:

    private void spawnBlock(Type type){
       currentBlock = this.registry.get(type).get();
    }
    

    【讨论】:

    • “生成”(如“实例化”new)块,但总是返回相同的实例。为了处理这个问题,这张地图的值将/可能/应该类似于Supplier&lt;Block&gt;...
    【解决方案3】:

    有人告诉我尽可能避免使用 switch 语句

    这是正确的,但你在这里所做的可能没问题。

    重点是:您不想到处都有这样的 switch 语句。它们很糟糕,因为它们将事物结合在一起。当您一遍又一遍地重复“相同”的开关模式时,这可能会变成维护的噩梦。因为对枚举类型的更改...意味着您必须查看每个开关以确定是否需要对其进行调整。

    因此,当您能够隐藏大部分代码中的此类切换(仅在少数几个地方进行,理想情况下,在一个地方进行),您就可以了。

    不过,更好的方法是用这个单行替换整个 switch 语句:

    currentBlock = type.createBlock();
    

    换句话说:为什么不将知识直接放入枚举类本身?当然,这还有其他含义,例如可能违反单一责任原则

    但它感觉很自然,表示不同类型块的枚举也提供了创建此类块的方法(鉴于 type 是唯一的确定您需要哪种 Block 子类的信息)。

    请注意:您不一定移动 开关 到枚举中。您可以完全避免在这里切换:

    enum Type {
      I(IBlock::new), L(LBlock::new), ...;
    
      private Supplier<? extends Block> supplier;
      private Type(Supplier<? extends Block> supplier) {
        this.supplier = supplier;
      }
    
      public Block getBlock() {
        return supplier.get();
      }
    

    (我没有通过编译器运行以上代码,所以请注意拼写错误)

    【讨论】:

    • 哇,我从来没有想过将方法写入枚举类本身。我将我的 switch 语句移到了那里,如下所示: public createBlock(){switch(this){...
    • @orchid 感谢您的快速接受,尽管我不希望您将开关移动到枚举中。请参阅我刚刚添加到答案中的代码。您可以提供 Supplier 接口的实例作为该枚举的成员,然后创建一个块转换为:调用该供应商!
    • @orchid 这是不明智的,特别是如果该方法仍然是基于实例的。现在每个常量都知道所有其他常量。
    • @AndrewTobilko 你说“这不明智”......“它”是什么?
    • @orchid 将该方法作为实例方法移动到枚举
    【解决方案4】:

    既然你已经有了一个枚举,就让每个常量知道如何创建它所代表的类型的实例。

    interface Block {}
    class IBlock implements Block {}
    class LBlock implements Block {}
    
    enum  Type {
      I(IBlock::new), 
      L(LBlock::new);
    
      private Supplier<? extends Block> blockSupplier;
    
      Type(Supplier<? extends Block> supplier) {
        blockSupplier =supplier;
      }
    
      public Supplier<? extends Block> getBlockSupplier() {
        return blockSupplier;
      }
    }
    

    生成块的方法如下所示

    private void spawnBlock(Type type) {
      currentBlock = type.getBlockSupplier().get();
    }
    

    话虽如此,我认为这里的切换方法没有问题。更令人担忧的是该方法有一个副作用:它不会产生新的Block(我会使用它),它会设置字段。一个名为spawnBlock 的方法什么都不返回,会有什么期望?

    【讨论】:

    • 有任何理由公开Supplier,而不仅仅是隐藏它并通过type.createBlock() 方法提供其功能?
    • @Marco13 只是让调用者决定何时创建对象
    • 获取供应商会公开“实施细节”。它使call().somewhat().longer()。没有人可以有正当理由获得供应商,除非他想创建一个实例。 (如果有人想要一个Supplier 实例,例如传递它,他仍然可以创建一个Supplier&lt;Block&gt; supplier = type::createBlock 方法引用)。抱歉,我没有理由这样做...
    • @Marco13 重点在于惰性初始化,这里不是很清楚,因为我们在获得type.getBlockSupplier() 后立即调用get。想象一下,您正准备创建一个块,突然用户获得了更改下一个块的类型/暂停游戏 10 秒/完成游戏等的奖金。您处理掉供应商并继续前进。
    • @Marco13 获得供应商意味着您可能想要创建一个对象。但这并不一定意味着您会创建一个。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-12-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-04-30
    相关资源
    最近更新 更多