Java中的switch语句正在被改变。但是,这究竟是一种升级还是一种混乱?
经典的开关
Java中的经典switch语句并不是很好。 与Java的许多其他部分不同,它在多年前从C语言中提取功能时并没有进行适当的反思。
case
关键的缺陷是 "fall-through-by-default"。这意味着,如果你忘记在每个break
子句,处理将继续到下一个case
子句。
另一个缺陷是变量的范围是整个开关,因此你不能在两个不同的case
子句中重复使用一个变量名。此外,default
子句不是必须的,这使得代码的读者不清楚是否有子句被遗忘。
当然,还有一个关键的限制--要切换的类型只能是整数、枚举或字符串。
String instruction;
switch (trafficLight) {
case RED:
instruction = "Stop";
case YELLOW:
instruction = "Prepare";
break;
case GREEN:
instruction = "Go";
break;
}
System.out.println(instruction);
复制代码
上面的代码不能编译,因为没有default
子句,导致instruction
未定义。但即使它编译了,由于缺少break
,它也不会打印 "Stop"。 在我自己的编码中,我喜欢总是把开关放在方法的末尾,每个子句都包含一个return
,以减少开关的风险。
升级后的switch
作为Project Amber的一部分,switch正在被升级。 但遗憾的是,我对新设计的优点不以为然。 说白了,有一些好的方面,但总的来说,我认为这个解决方案过于复杂,而且有一些令人不快的语法选择。
关键的目的是增加一个_表达式_,你可以把切换的结果分配给一个变量。这就像三元操作符(例如:x != null ? x : ""
),它相当于if语句的表达式。表达式将减少类似上述未定义变量的问题,因为它使每个分支必须产生一个变量的情况更加明显。
目前的计划是增加不是一种,而是三种新的开关形式。是的,三种。
在一篇博文中解释这一点,不出所料,要花点时间......
- 类型1:具有经典语法的声明。和今天一样。有默认的下降。不是详尽的。
- 类型2:具有经典语法的表达式。新!有默认的落差。必须是详尽的。
- 类型3:具有新语法的语句。新!没有落差。非穷尽性。
- 类型4:具有新语法的表达式。新!没有落差。必须是详尽的。
标题的例子(类型4)当然是相当不错的。
// type 4
var instruction = switch (trafficLight) {
case RED -> "Stop";
case YELLOW -> "Prepare";
case GREEN -> "Go";
};
System.out.println(instruction);
复制代码
可以看到,类型3和类型4的新语法使用箭头而不是冒号。 而且,如果代码由单个表达式组成,就不需要使用break
。当使用枚举时,也不需要default
子句,因为只要你包含了所有已知的枚举值,编译器就可以为你插入它。所以,如果你漏掉了GREEN
,你会得到一个编译错误。
当然,魔鬼是在细节中的。
首先,一个明显的积极因素。与其因为列出多个标签而落空,不如用逗号来分隔它们。
// type 4
var instruction = switch (trafficLight) {
case RED, YELLOW -> "Stop";
case GREEN -> "Go";
};
System.out.println(instruction);
复制代码
直截了当,显而易见。并避免了许多简单的掉线用例。
如果要执行的代码比表达式更复杂怎么办?
// type 4
var instruction = switch (trafficLight) {
case RED -> "Stop";
case YELLOW -> {
revYourEngine();
yield "Prepare";
}
case GREEN -> "Go";
};
System.out.println(instruction);
复制代码
yield
?耸耸肩。在很长一段时间里,它将是break {expression}
,但这与标记的中断(一个很少使用的语法特征)相冲突。
那么类型2呢?
// type 2
var instruction = switch (trafficLight) {
case RED: yield "Stop";
case YELLOW:
System.out.println("Prepare");
case GREEN: yield "Go";
};
System.out.println(instruction);
复制代码
哎呀!我忘了yield
。所以,输入YELLOW会输出 "准备",然后落空,产生 "去"。
那么,为什么建议增加一种新的开关形式,重复20年前的错误呢? 答案是正交性--一个2x2的网格,表达式与语句,默认掉线与不掉线。
一个关键的问题是,正交性是否能证明在语言中添加一个几乎完全无用的开关形式(类型2)。
那么,类型3就可以了吗?
嗯,不是。由于坚持正交性,因此坚持复制与类型1语句切换有关的历史规则,没有要求列出所有case
。
// type 3
switch (trafficLight) {
case RED -> doStop();
case GO -> doGo();
}
复制代码
那么,对于黄色会发生什么?答案是什么都没有,但是作为读者,我不禁要问,这段代码是正确的还是不完整的。如果上述情况是一个编译错误,开发人员被迫写一个default
子句,那就好得多。
// type 3
switch (trafficLight) {
case RED -> doStop();
case GO -> doGo();
default -> {}
}
复制代码
官方的说法是,既然1型语句切换(目前的)不能强制执行穷尽性,那么新的3型语句切换也不能。 我的看法是,保留20年前的糟糕设计是更大的罪过。
还有什么?嗯,需要记住的一点是,表达式不能提前完成,因此没有办法直接从switch表达式(类型2或4)中return
。也没有办法继续/中断一个循环。相信我,当我说在实际应用的规则中,有无穷无尽的Java考试问题。
总结一下这些类型
类型1:经典的语句切换
- 语句
- 通过默认的方式实现落地
return
,也允许继续/中断一个循环- 变量的单一范围
- 每种情况下的逻辑是一连串的语句,可能以下列方式结束
break
- 并非详尽无遗--不需要默认条款
yield
是不允许的
类型2:经典语法表达式切换
- 表达式
- 通过默认的方式实现
return
,不允许,不能继续/中断一个循环- 变量的单一范围
- 每种情况下的逻辑可以是一个产量表达式,或者是一个可能以下列内容结束的语句序列
yield
- 穷举--默认条款是必须的
- 必须使用
yield
来返回值
类型3:箭头形式的语句切换
- 语句
- 不允许落空
return
,也允许继续/中断一个循环- 没有变量范围问题,每种情况的逻辑必须是一个语句或一个块
- 不是详尽的--不需要默认条款
yield
是不允许的
类型4:箭头形式的表达式切换
- 表达式
- 不允许回避
return
不允许,不能继续/中断循环- 没有变量范围问题,每种情况下的逻辑必须是一个表达式或一个块,结尾是
yield
- 穷举--默认条款是必须的
- 必须使用
yield
来返回值,但只能从块中返回(当不是块时,它是隐含的)。
你困惑了吗?
好吧,我肯定我没有完美地解释一切,而且我很可能在某个地方犯了一个错误。但现实是,它很复杂,有很多规则隐藏在众目睽睽之下。 是的,它是正交的。但我真的不认为这有助于理解这个特征。
我会怎么做呢?
第4类switch表达式很好(尽管我对从lambdas延伸出来的箭头语法确实有意见)。 我的问题是第2类和第3类。在现实中,这两类switch
,会非常罕见,因此大多数开发者都不会看到它们。鉴于此,我认为最好是完全不包括它们。一旦接受这一点,将表达式形式作为switch
,就没有意义了,因为它实际上不会与旧的语句形式有很多联系。
我将放弃类型2和3,并允许类型4的开关表达式成为所谓的_语句表达式_。(语句表达式的另一个例子是方法调用,它可以作为表达式或作为自己一行的语句使用,忽略任何返回值)。
// Stephen's expression switch
var instruction = match (trafficLight) {
case RED: "Stop";
case YELLOW: "Prepare";
case GO: "Go";
}
// Stephen's expression switch used as a statement (technically a statement expression)
match (instruction) {
case "Stop": doStop();
case "Go": doGo();
default: ;
}
复制代码
我的方法使用了一个新的关键字match
,因为我认为扩展switch
是错误的使用基线。把它变成一个语句表达式意味着只有一套规则--它总是一个表达式,只是你可以把它当作一个语句来使用。用我的方法做不到的是在语句版本中使用return
,因为它实际上不是一个语句(今天你不能从 Java 的任何表达式中使用return
,所以这也没什么不同)。
总结
如果你忽略了复杂性,而只是使用4型开关表达式,那么这个新特性是相当合理的。
然而,为了增加Java所需要的一种开关形式,我们还得到了另外两个哑巴--2型和3型。 在我看来,这个功能需要回到绘图板,但遗憾的是,我怀疑现在已经太晚了。