gpt4 book ai didi

How to ensure completeness in an enum switch at compile time?(如何在编译时确保枚举开关的完整性?)

转载 作者:bug小助手 更新时间:2023-10-25 10:57:33 26 4
gpt4 key购买 nike



I have several switch statements which test an enum. All enum values must be handled in the switch statements by a case statement. During code refactoring it can happen that the enum shrinks and grows. When the enum shrinks the compiler throws an error. But no error is thrown, if the the enum grows. The matching state gets forgotten and produces a run time error. I would like to move this error from run time to compile time. Theoretically it should be possible to detect the missing enum cases at compile time. Is there any way to achieve this?

我有几个用于测试枚举的Switch语句。在Switch语句中,所有枚举值都必须由CASE语句处理。在代码重构期间,可能会发生枚举收缩和增长的情况。当枚举收缩时,编译器会抛出错误。但是,如果枚举增长,则不会引发错误。匹配状态会被遗忘并产生运行时错误。我想将这个错误从运行时移到编译时。从理论上讲,应该可以在编译时检测丢失的枚举用例。有什么办法可以做到这一点吗?



The question exists already "How to detect a new value was added to an enum and is not handled in a switch" but it does not contain an answer only an Eclipse related work around.

这个问题已经存在“如何检测添加到枚举中的新值,而不是在开关中处理”,但它不包含答案,只包含一个与Eclipse相关的解决方法。


更多回答

Presumably you don't have a default case?

想必你没有默认案例吧?

@OliCharlesworth How do you think I got the run time error? ;-)

@OliCharlesworth你认为我是如何得到运行时错误的?;-)

Well, if you have a default case, then it's unlikely that any compile-time tool would warn you about "missing" cases (because they're not actually missing in that scenario ;) )

嗯,如果您有一个默认的用例,那么任何编译时工具都不太可能警告您“缺少”用例(因为在该场景中它们实际上并没有丢失;)

Write a test that fails if one case is missing. Run the tests regularly.

编写一个测试,如果缺少一个案例,该测试将失败。定期进行测试。

@vikingsteve that would require writing tests for something the compiler should catch. Even if someone wanted/tried to do this, what would catch when a test is missing? That is why they are asking this question, and why 7 years later people like me are searching for this kind of thing and finding this question.

@vikingsteve,这将需要为编译器应该捕获的东西编写测试。即使有人想要/尝试这样做,当测试丢失时会有什么问题呢?这就是为什么他们会问这个问题,为什么7年后像我这样的人会寻找这种东西,找到这个问题。

优秀答案推荐

In Effective Java, Joshua Bloch recommends creating an abstract method which would be implemented for each constant. For example:

在Efficient Java中,Joshua Bloch建议创建一个抽象方法,该方法将为每个常量实现。例如:


enum Color {
RED { public String getName() {return "Red";} },
GREEN { public String getName() {return "Green";} },
BLUE { public String getName() {return "Blue";} };
public abstract String getName();
}

This would function as a safer switch, forcing you to implement the method if you add a new constant.

这将是一个更安全的开关,如果您添加了一个新常量,则会强制您实现该方法。


EDIT: To clear up some confusion, here's the equivalent using a regular switch:

编辑:为了澄清一些困惑,下面是使用常规开关的等价物:


enum Color {
RED, GREEN, BLUE;
public String getName() {
switch(this) {
case RED: return "Red";
case GREEN: return "Green";
case BLUE: return "Blue";
default: return null;
}
}
}


Another solution uses the functional approach. You just need to declare the enum class according with next template:

另一种解决方案使用函数方法。您只需要根据下一个模板声明枚举类:



public enum Direction {

UNKNOWN,
FORWARD,
BACKWARD;

public interface SwitchResult {
public void UNKNOWN();
public void FORWARD();
public void BACKWARD();
}

public void switchValue(SwitchResult result) {
switch (this) {
case UNKNOWN:
result.UNKNOWN();
break;
case FORWARD:
result.FORWARD();
break;
case BACKWARD:
result.BACKWARD();
break;
}
}
}


If you try to use this without one enumeration constant at least, you will get the compilation error:

如果尝试至少在不使用一个枚举常量的情况下使用它,则会出现编译错误:



getDirection().switchValue(new Direction.SwitchResult() {
public void UNKNOWN() { /* */ }
public void FORWARD() { /* */ }
// public void BACKWARD() { /* */ } // <- Compilation error if missing
});


I don't know about the standard Java compiler, but the Eclipse compiler can certainly be configured to warn about this. Go to Window->Preferences->Java->Compiler->Errors/Warnings/Enum type constant not covered on switch.

我不知道标准的Java编译器是怎么样的,但是Eclipse编译器肯定可以被配置为对此发出警告。进入窗口->首选项->Java->枚举器->错误/枚举/枚举类型常量未覆盖开关。



You could also use an adaptation of the Visitor pattern to enums, which avoid putting all kind of unrelated state in the enum class.

您还可以使用访问者模式对枚举的改编,从而避免将所有类型的不相关状态放入枚举类中。



The compile time failure will happen if the one modifying the enum is careful enough, but it is not garanteed.

如果修改枚举的人足够小心,编译时会失败,但这并不保证。



You'll still have a failure earlier than the RTE in a default statement : it will fail when one of the visitor class is loaded, which you can make happen at application startup.

在default语句中,在RTE之前仍然会有一个失败:当加载一个访问者类时,它会失败,这可以在应用程序启动时发生。



Here is some code :

以下是一些代码:



You start from an enum that look like that :

您可以从如下所示的枚举开始:



public enum Status {
PENDING, PROGRESSING, DONE
}


Here is how you transform it to use the visitor pattern :

下面是如何将其转换为使用访问者模式:



public enum Status {
PENDING,
PROGRESSING,
DONE;

public static abstract class StatusVisitor<R> extends EnumVisitor<Status, R> {
public abstract R visitPENDING();
public abstract R visitPROGRESSING();
public abstract R visitDONE();
}
}


When you add a new constant to the enum, if you don't forget to add the method visitXXX to the abstract StatusVisitor class, you'll have directly the compilation error you expect everywhere you used a visitor (which should replace every switch you did on the enum) :

当您向枚举添加新的常量时,如果您没有忘记将方法visitXXX添加到抽象的StatusVisitor类,您将在使用访问器的任何地方直接出现预期的编译错误(它应该替换您在枚举上所做的每个开关):



switch(anObject.getStatus()) {
case PENDING :
[code1]
break;
case PROGRESSING :
[code2]
break;
case DONE :
[code3]
break;
}


should become :

应该成为:



StatusVisitor<String> v = new StatusVisitor<String>() {
@Override
public String visitPENDING() {
[code1]
return null;
}
@Override
public String visitPROGRESSING() {
[code2]
return null;
}
@Override
public String visitDONE() {
[code3]
return null;
}
};
v.visit(anObject.getStatus());


And now the ugly part, the EnumVisitor class. It is the top class of the Visitor hierarchy, implementing the visit method and making the code fail at startup (of test or application) if you forgot to update the absract visitor :

现在是难看的部分,EnumVisitor类。它是Visator层次结构的顶层类,实现了Access方法,并在您忘记更新无效访问者的情况下使代码在启动(测试或应用程序)时失败:



public abstract class EnumVisitor<E extends Enum<E>, R> {

public EnumVisitor() {
Class<?> currentClass = getClass();
while(currentClass != null && !currentClass.getSuperclass().getName().equals("xxx.xxx.EnumVisitor")) {
currentClass = currentClass.getSuperclass();
}

Class<E> e = (Class<E>) ((ParameterizedType) currentClass.getGenericSuperclass()).getActualTypeArguments()[0];
Enum[] enumConstants = e.getEnumConstants();
if (enumConstants == null) {
throw new RuntimeException("Seems like " + e.getName() + " is not an enum.");
}
Class<? extends EnumVisitor> actualClass = this.getClass();
Set<String> missingMethods = new HashSet<>();
for(Enum c : enumConstants) {
try {
actualClass.getMethod("visit" + c.name(), null);
} catch (NoSuchMethodException e2) {
missingMethods.add("visit" + c.name());
} catch (Exception e1) {
throw new RuntimeException(e1);
}
}
if (!missingMethods.isEmpty()) {
throw new RuntimeException(currentClass.getName() + " visitor is missing the following methods : " + String.join(",", missingMethods));
}
}

public final R visit(E value) {
Class<? extends EnumVisitor> actualClass = this.getClass();
try {
Method method = actualClass.getMethod("visit" + value.name());
return (R) method.invoke(this);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}


There are several ways you could implement / improve this glue code. I choose to walk up the class hierarchy, stop when the superclass is the EnumVisitor, and read the parameterized type from there. You could also do it with a constructor param being the enum class.

有几种方法可以实现/改进这个粘合代码。我选择在类层次结构中向上遍历,在超类是EnumVisitor时停下来,并从那里读取参数化类型。您也可以使用构造函数参数作为枚举类来完成此操作。



You could use a smarter naming strategy to have less ugly names, and so on...

您可以使用更智能的命名策略来获得不那么难听的名称,等等。



The drawback is that it is a bit more verbose.
The benefits are

缺点是它有点冗长。好处是




  • compile time error [in most cases anyway]

  • works even if you don't own the enum code

  • no dead code (the default statement of switch on all enum values)

  • sonar/pmd/... not complaining that you have a switch statement without default statement



The Enum Mapper project provides an an annotation processor which will make sure at compile-time that all enum constants are handled.

Moreover it supports reverse lookup and paritial mappers.

Enum Mapper项目提供了一个注释处理器,它将确保在编译时处理所有的Enum常量。此外,它还支持反向查找和独立映射器。



Usage example:

使用示例:



@EnumMapper
public enum Seasons {
SPRING, SUMMER, FALL, WINTER
}


The annotation processor will generate a java class Seasons_MapperFull, which can be used to map all enum constants to arbitrary values.

批注处理器将生成一个Java类Seasons_MapperFull,它可用于将所有枚举常量映射到任意值。



Here is an example use where we map each enum constant to a string.

下面是一个将每个枚举常量映射到一个字符串使用示例。



EnumMapperFull<Seasons, String> germanSeasons = Seasons_MapperFull
.setSPRING("Fruehling")
.setSUMMER("Sommer")
.setFALL("Herbst")
.setWINTER("Winter");


You can now use the mapper to get the values, or do reverse lookup

现在,您可以使用映射器获取值,或执行反向查找



String germanSummer = germanSeasons.getValue(Seasons.SUMMER); // returns "Sommer"
ExtremeSeasons.getEnumOrNull("Sommer"); // returns the enum-constant SUMMER
ExtremeSeasons.getEnumOrRaise("Fruehling"); // throws an IllegalArgumentException


Probably a tool like FindBugs will mark such switches.

也许像FindBugs这样的工具会标记这样的开关。



The hard answer would be to refactor:

最难的答案是重构:



Possibility 1: can go Object Oriented

可能性1:可以转向面向对象



If feasible, depends on the code in the cases.

如果可行,则取决于案例中的代码。



Instead of

而不是



switch (language) {
case EO: ... break;
case IL: ... break;
}


create an abstract method:, say p

创建抽象方法:,比方说p



language.p();


or



switch (p.category()) {
case 1: // Less cases.
...
}


Possibility 2: higher level

可能性2:更高级别



When having many switches, in an enum like DocumentType, WORD, EXCEL, PDF, ... .
Then create a WordDoc, ExcelDoc, PdfDoc extending a base class Doc. And again one can work object oriented.

当有许多开关时,在类似DocumentType、Word、Excel、PDF等的枚举中...然后创建一个扩展基类Doc的WordDoc、ExcelDoc、PdfDoc。同样,人们也可以使用面向对象的方法。



In my opinion and if the code that your are going to execute is outside of the domain of your enum, a way to do that is to build a unit test case that loops through your items in the enumeration and execute the piece of code that contains the switch.If something goes wrong or not as expected you can check the return value or the state of the object with an assertion.

在我看来,如果要执行的代码不在枚举的域内,一种方法是构建一个单元测试用例,该用例循环遍历枚举中的项,并执行包含开关的代码段。如果出现错误或没有如预期的那样出错,可以使用断言检查返回值或对象的状态。



You could execute the tests as part of some building process and you will see any anomalies at this point.

您可以将测试作为构建过程的一部分执行,此时您将看到任何异常。



Anyway, unit testing is almost mandatory and beneficial in many projects.

无论如何,单元测试在许多项目中几乎是强制性的,并且是有益的。



If the code inside the switch belongs in the enum, include it within as proposed in other answers.

如果交换机内的代码属于枚举,请按照其他答案中的建议将其包括在内。



Since Java 14 (with JEP 361) there are now Switch Expressions which lets you do that. For example with the following example:

自从Java 14(带有JEP 361)以来,现在有了Switch表达式,可以让您做到这一点。例如,使用以下示例:


public enum ExampleEnum {A, B}

public static String exampleMethod(ExampleEnum exampleEnum) {
return switch (exampleEnum) {
case A -> "A";
case B -> "B";
};
}

If you add a new enum value C then you get a compile error: java: the switch expression does not cover all possible input values.

如果添加新的枚举值C,则会出现编译错误:Java:开关表达式没有涵盖所有可能的输入值。


But be warned, this does not magically work with "normal" (old) switch statements like:

但请注意,这并不能神奇地与“普通”(旧)开关语句一起使用,例如:


public static String badExampleMethod(ExampleEnum exampleEnum) {
switch (exampleEnum) {
case A:
return "A";
case B:
return "B";
}
}

This would already throw a compile error: java: missing return statement and so force you to add a default branch (which then does not bring a compile error when adding a new enum value).

这已经抛出了一个编译错误:Java:Missing Return语句,因此强制您添加一个默认分支(这样在添加新枚举值时不会带来编译错误)。



If you're using Android Studio (at least version 3 and up) you can activate this exact check in the inspections setting. This might be available on other IntelliJ Java IDE's as well.

如果你使用的是Android Studio(至少是版本3和更高版本),你可以在检查设置中激活这个精确的检查。这可能在其他IntelliJ Java IDE上也可用。



Go to Preferences/Inspections. In the Java/Control flow Issues section, check the item Enum 'switch' statement that misses case. Optionally you can change severity to Error to make it more obvious than a warning.

转到首选项/检查。在Java/Control flow Issues部分中,检查缺少大小写的Item Enum‘Switch’语句。或者,您可以将严重性更改为错误,以使其比警告更明显。



I know the question is about Java, and I think the answer for pure Java is clear: it's not a built-in feature, but there are workarounds. For those who arrive here and are working on Android or other systems that can utilize Kotlin, that language provides this feature with its when expression, and the interop with Java allows it to be rather seamless, even if this is the only Kotlin code in your codebase.

我知道问题是关于Java的,我认为纯Java的答案很清楚:它不是一个内置的功能,但有解决办法。对于那些来到这里并在Android或其他可以使用Kotlin的系统上工作的人来说,该语言提供了这个功能和它的When表达式,并且与Java的互操作允许它相当无缝,即使这是您代码库中唯一的Kotlin代码。



For example:

例如:



public enum HeaderSignalStrength {
STRENGTH_0, STRENGTH_1, STRENGTH_2, STRENGTH_3, STRENGTH_4;
}


With my original Java code as:

我的原始Java代码是:



// In HeaderUtil.java
@DrawableRes
private static int getSignalStrengthIcon(@NonNull HeaderSignalStrength strength) {
switch (strength) {
case STRENGTH_0: return R.drawable.connection_strength_0;
case STRENGTH_1: return R.drawable.connection_strength_1;
case STRENGTH_2: return R.drawable.connection_strength_2;
case STRENGTH_3: return R.drawable.connection_strength_3;
case STRENGTH_4: return R.drawable.connection_strength_4;
default:
Log.w("Unhandled HeaderSignalStrength: " + strength);
return R.drawable.cockpit_connection_strength_0;
}
}

// In Java code somewhere
mStrength.setImageResource(HeaderUtil.getSignalStrengthIcon(strength));


Can be rewritten with Kotlin:

可以用Kotlin重写:



// In HeaderExtensions.kt
@DrawableRes
fun HeaderSignalStrength.getIconRes(): Int {
return when (this) {
HeaderSignalStrength.STRENGTH_0 -> R.drawable.connection_strength_0
HeaderSignalStrength.STRENGTH_1 -> R.drawable.connection_strength_1
HeaderSignalStrength.STRENGTH_2 -> R.drawable.connection_strength_2
HeaderSignalStrength.STRENGTH_3 -> R.drawable.connection_strength_3
HeaderSignalStrength.STRENGTH_4 -> R.drawable.connection_strength_4
}
}

// In Java code somewhere
mStrength.setImageResource(HeaderExtensionsKt.getIconRes(strength));


Had the same issue. I throw an error on the default case and add a static initializer that iterates all enum values. Simple but fails fast. If you have some unit test coverage it does the trick.

也有同样的问题。我在缺省情况下抛出一个错误,并添加一个迭代所有枚举值的静态初始值设定项。很简单,但很快就会失败。如果您有一些单元测试覆盖率,它就能起到作用。


public class HolidayCalculations {

public static Date getDate(Holiday holiday, int year) {
switch (holiday) {
case AllSaintsDay:
case AscensionDay:
return new Date(1);
default:
throw new IllegalStateException("getDate(..) for "+holiday.name() + " not implemented");

}
}

static {
for (Holiday value : Holiday.values()) getDate(value, 2000);
}

}


This is a variant of the Visitor approach which gives you compile-time help when you add constants:

这是访问者方法的一个变体,在添加常量时为您提供编译时帮助:



interface Status {
enum Pending implements Status {
INSTANCE;

@Override
public <T> T accept(Visitor<T> v) {
return v.visit(this);
}
}
enum Progressing implements Status {
INSTANCE;

@Override
public <T> T accept(Visitor<T> v) {
return v.visit(this);
}
}
enum Done implements Status {
INSTANCE;

@Override
public <T> T accept(Visitor<T> v) {
return v.visit(this);
}
}

<T> T accept(Visitor<T> v);
interface Visitor<T> {
T visit(Done done);
T visit(Progressing progressing);
T visit(Pending pending);
}
}

void usage() {
Status s = getRandomStatus();
String userMessage = s.accept(new Status.Visitor<String>() {
@Override
public String visit(Status.Done done) {
return "completed";
}

@Override
public String visit(Status.Progressing progressing) {
return "in progress";
}

@Override
public String visit(Status.Pending pending) {
return "in queue";
}
});
}


Beautiful, eh? I call it the "Rube Goldberg Architecture Solution".

很漂亮,是吧?我称之为“Rube Goldberg建筑解决方案”。



I would normally just use an abstract method, but if you really don't want to add methods to your enum (maybe because you introduce cyclic dependencies), this is a way.

我通常只使用抽象方法,但如果您真的不想将方法添加到枚举中(可能是因为您引入了循环依赖),这是一种方法。



Functional approach with lambdas, much less code

使用函数式方法,更少的代码


public enum MyEnum {
FIRST,
SECOND,
THIRD;

<T> T switchFunc(
Function<MyEnum, T> first,
Function<MyEnum, T> second,
Function<MyEnum, T> third
// when another enum constant is added, add another function here
) {
switch (this) {
case FIRST: return first.apply(this);
case SECOND: return second.apply(this);
case THIRD: return third.apply(this);
// and case here
default: throw new IllegalArgumentException("You forgot to add parameter");
}
}

public static void main(String[] args) {
MyEnum myEnum = MyEnum.FIRST;

// when another enum constant added method will break and trigger compile-time error
String r = myEnum.switchFunc(
me -> "first",
me -> "second",
me -> "third");
System.out.println(r);
}

}

}



In case there are several enums on different tiers of the project that must correspond to each other, this can be ensured by a test case:

如果在项目的不同层上有几个必须相互对应的枚举,这可以通过测试用例来确保:



private static <T extends Enum<T>> String[] names(T[] values) {
return Arrays.stream(values).map(Enum::name).toArray(String[]::new);
}

@Test
public void testEnumCompleteness() throws Exception {
Assert.assertArrayEquals(names(Enum1.values()), names(Enum2.values()));
}

更多回答

It would be useful if you would have shown the corresponding switch. From what you say I got the idea that I have to sub class the original enum in every place where a switch of the enum is used. Is this correct? Sounds like much overhead.

如果您已经显示了相应的开关,这将非常有用。从你所说的,我得到了一个想法,我必须在每个使用枚举开关的地方将原始枚举子类化。这样对吗?听起来开销很大。

@ceving No, you can't subclass an enum. My assumption was that you have access to the code, in which case it may be preferable to build the switch into the enum itself. See above for an equivalent version implemented as a standard switch.

@ceving否,您不能将枚举细分为子类。我的假设是您可以访问代码,在这种情况下,最好将开关构建到枚举本身中。有关作为标准交换机实现的等效版本,请参阅上面的内容。

Ok this is possible (I tried it and it works) but if I have more than one switch, I move unrelated code from different places in a central enum class. It works but the code does not belong there.

好的,这是可能的(我试过了,它起作用了),但如果我有多个开关,我会将不相关的代码从一个中央枚举类中的不同位置移走。它可以工作,但代码不属于那里。

This reduces n switch statements to 1 switch statement. It is not a perfect solution but a big improvement. But I think it might be better to call SwitchResult Directable and switchValue direct, because I don't think this solution can be abstracted into a template.

这将n个Switch语句减少为1个Switch语句。这不是一个完美的解决方案,而是一个很大的进步。但我认为将SwitchResult称为Directable和SwitchValue Direct可能更好,因为我认为这个解决方案不能抽象到模板中。

@ceving: Ah ok. You should probably update your question to indicate that you're explicitly interested in javac (or whichever compiler) then...

@ceving:啊好的。您可能应该更新您的问题,以表明您对javac(或任何编译器)都有明确的兴趣。

Yes I had the same idea and added the link the the other question.

是的,我也有同样的想法,并在另一个问题上添加了链接。

@ceving: Indeed, I saw that. But you haven't actually said which compiler you're actually interested in (without this information, your question is just a duplicate of that other one ;) )

@ceving:的确,我看到了。但是您实际上还没有说您实际上对哪个编译器感兴趣(如果没有这个信息,您的问题只是另一个问题的副本;)

@ceving: Well, Eclipse comes with its own compiler, which may be invoked standalone from the command-line if required. (Google for "Eclipse ECJ")

@ceving:嗯,Eclipse附带了自己的编译器,如果需要,可以从命令行独立调用该编译器。(谷歌搜索“月食ECJ”)

I think in your possibility 1 it is still possible to miss a "category" at compile time.

我认为在您的可能性1中,仍有可能在编译时遗漏一个“类别”。

It's probably worth mentioning that p() would need to be marked abstract in the base class.

可能值得一提的是,p()需要在基类中标记为抽象。

What does this help? If category() can return 1, 3, and 5 who checks that the switch contains for 1, 3 and 5 a matching case expression?

这有什么帮助呢?如果CATEGORY()可以返回1、3和5,谁会检查开关是否包含1、3和5的匹配CASE表达式?

(1) If an implementation is indeed needed, yes abstract - thanks @OliCharlesworth. It depends on whether normally a switch is added or an enum value is added. (2) If many cases are the same, having a switch with say just 3 categories is less error prone, and the number of categories will not grow as fast if ever. All this is merely a software engineering code style argumentation. Whether sensible depends.

(1)如果确实需要一个实现,那么就是抽象的,谢谢@OliCharlesworth。这取决于通常是添加开关还是添加枚举值。(2)如果许多情况都是相同的,那么只有3个类别的切换就不太容易出错,而且类别的数量也不会增长得那么快。所有这些只是一种软件工程代码风格的论证。是否明智取决于。

Using unit tests to fix language problems smells at bit like a hack.

使用单元测试来修复语言问题听起来有点像黑客。

it is indeed, but there is hope with the new switch expressions.

的确如此,但新的转变表达方式带来了希望。

I am not sure, if I understand this. Is this one enum with three values or three enums with one value?

我不确定,我是否理解了这一点。这是具有三个值的一个枚举还是具有一个值的三个枚举?

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com