¿Hay alguna forma de forzar una verificación exhaustiva de todos los valores de enumeración cuando el interruptor se bifurca en métodos de llamada con tipo de retorno nulo? Es bastante feo codificar un rendimiento solo para convencer al compilador de que exija exhaustividad.
Este es mi patrón actual (los métodos de manejo tienen un tipo de retorno nulo)
int unused = switch (event.getEventType()) { case ORDER -> { handle((OrderEvent) event); yield 0; } case INVOICE -> { handle((InvoiceEvent) event); yield 0; } case PAYMENT -> { handle((PaymentEvent) event); yield 0; } };La razón por la que quiero usar una expresión es para obtener un error de compilación cuando se agrega un nuevo valor de enumeración y no se maneja.
Tal vez produzca un Consumer of Event , por lo que produce algo útil, la compensación es una línea más para consumer.accept .
Consumer<Event> consumer = switch (event.getEventType()) { case ORDER -> e -> handle((OrderEvent) e); case INVOICE -> e -> handle((InvoiceEvent) e); case PAYMENT -> e -> handle((PaymentEvent) e); }; consumer.accept(event);Según el comentario sobre la penalización de rendimiento, se realiza un punto de referencia para comparar los siguientes escenarios:
Para ver
handle estático y de instancia?Y el resultado es:
# Run complete. Total time: 00:20:30 Benchmark Mode Cnt Score Error Units SwitchExpressionBenchMark.consumerHandle thrpt 300 49343.496 ± 91.324 ops/ms SwitchExpressionBenchMark.consumerStaticHandle thrpt 300 49312.273 ± 112.630 ops/ms SwitchExpressionBenchMark.noConsumerHandle thrpt 300 49353.232 ± 106.522 ops/ms SwitchExpressionBenchMark.noConsumerStaticHandle thrpt 300 49496.614 ± 122.916 ops/msAl observar el resultado, no hay mucha diferencia entre los 4 escenarios.
handle estático y de instancia es despreciable. El benchmark se realiza con:
UPC: Intel(R) Core(TM) i7-8750H
Memoria: 16G
Versión JMH: 1.19
Versión de máquina virtual: JDK 15.0.2
import org.openjdk.jmh.annotations.*; import org.openjdk.jmh.infra.Blackhole; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; @BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.MILLISECONDS) @State(Scope.Thread) @Warmup(iterations = 30, time = 500, timeUnit = TimeUnit.MILLISECONDS) @Measurement(iterations = 30, time = 500, timeUnit = TimeUnit.MILLISECONDS) public class SwitchExpressionBenchMark { public static void main(String[] args) throws Exception { org.openjdk.jmh.Main.main(args); } @Benchmark public void consumerStaticHandle(Blackhole blackhole, InvoiceEvent invoiceEvent) { Event event = invoiceEvent; Consumer<Event> consumer = switch (event.getEventType()) { case ORDER -> e -> staticHandle((OrderEvent) e); case INVOICE -> e -> staticHandle((InvoiceEvent) e); case PAYMENT -> e -> staticHandle((PaymentEvent) e); }; consumer.accept(event); } @Benchmark public void consumerHandle(Blackhole blackhole, InvoiceEvent invoiceEvent) { Event event = invoiceEvent; Consumer<Event> consumer = switch (event.getEventType()) { case ORDER -> e -> this.handle((OrderEvent) e); case INVOICE -> e -> this.handle((InvoiceEvent) e); case PAYMENT -> e -> this.handle((PaymentEvent) e); }; consumer.accept(event); } @Benchmark public void noConsumerHandle(Blackhole blackhole, InvoiceEvent invoiceEvent) { Event event = invoiceEvent; int unused = switch (event.getEventType()) { case ORDER -> { this.handle((OrderEvent) event); yield 0; } case INVOICE -> { this.handle((InvoiceEvent) event); yield 0; } case PAYMENT -> { this.handle((PaymentEvent) event); yield 0; } }; } @Benchmark public void noConsumerStaticHandle(Blackhole blackhole, InvoiceEvent invoiceEvent) { Event event = invoiceEvent; int unused = switch (event.getEventType()) { case ORDER -> { staticHandle((OrderEvent) event); yield 0; } case INVOICE -> { staticHandle((InvoiceEvent) event); yield 0; } case PAYMENT -> { staticHandle((PaymentEvent) event); yield 0; } }; } private static void staticHandle(PaymentEvent event) { doSomeJob(); } private static void staticHandle(InvoiceEvent event) { doSomeJob(); } private static void staticHandle(OrderEvent event) { doSomeJob(); } private void handle(PaymentEvent event) { doSomeJob(); } private void handle(InvoiceEvent event) { doSomeJob(); } private void handle(OrderEvent event) { doSomeJob(); } private static void doSomeJob() { Blackhole.consumeCPU(16); } private enum EventType { ORDER, INVOICE, PAYMENT } public static class Event { public EventType getEventType() { return eventType; } public void setEventType(EventType eventType) { this.eventType = eventType; } private EventType eventType; public double getD() { return d; } public void setD(double d) { this.d = d; } private double d; } public static class OrderEvent extends Event { } @State(Scope.Thread) public static class InvoiceEvent extends Event { @Setup(Level.Trial) public void doSetup() { this.setEventType(EventType.INVOICE); } } public static class PaymentEvent extends Event { } }Si tiene clases de prueba (por ejemplo, casos de prueba JUNIT) que crea y ejecuta antes de publicar su código principal, entonces puede colocar una función de protección simple en cualquier clase de prueba existente para cada enumeración que desee ver:
String checkForEnumChanged(YourEnum guard) { return switch (guard) { case ORDER -> "OK"; case INVOICE -> "OK"; case PAYMENT -> "OK"; }; } Esto significa que puede mantener el código de su aplicación principal libre del yield 0; estilo de interruptor y obtiene un error de compilación en las clases de prueba cuando se editan los valores de enumeración.
La declaración de la pregunta es un poco un "problema XY"; lo que quieres es la verificación de la totalidad, pero estás pidiendo que se trate como una expresión, no porque quieras una expresión, sino porque quieres la verificación de la totalidad que viene con la expresión.
Uno de los elementos de "deuda técnica" que quedan de la adición de expresiones de cambio es la capacidad de las sentencias de cambio de optar por la misma verificación de totalidad que obtienen las expresiones de cambio. No pudimos cambiar retroactivamente esto acerca de las declaraciones de cambio (siempre se ha permitido que las declaraciones de cambio sean parciales), pero tiene razón en que sería bueno poder obtener este tipo de verificación de tipo. Como supones, convertirlo en un interruptor de expresión vacía es una forma de llegar allí, pero de hecho es feo y, lo que es peor, no será fácil de descubrir. Está en nuestra lista encontrar una manera de permitirle volver a optar por la verificación de la totalidad para las declaraciones de cambio. Ha habido discusiones en la lista amber-spec-experts sobre esto; está relacionado con varias otras características posibles, y las discusiones de diseño aún están en curso.
Agregue un método de delegado para reenviar la solicitud y devolver un tipo Void
public class SwitchTest { enum EventType { ORDER, INVOICE, PARCELDELIVERY } interface Event { EventType getType(); } static class OrderType implements Event { @Override public EventType getType() { return EventType.ORDER; } } static class InvoiceType implements Event { @Override public EventType getType() { return EventType.INVOICE; } } static void handle(Event e) { System.out.println(e.getType()); } static Void switchExpressionDelegate(Event e) { handle(e); return null; } public static void main(String[] args) { Event event = new OrderType(); Void nullNoop = switch (event.getType()) { case ORDER -> switchExpressionDelegate(event); case INVOICE -> switchExpressionDelegate(event); case PARCELDELIVERY -> switchExpressionDelegate(event); }; } } Suponiendo que el método handle tiene un tipo exacto, se debe agregar una jerarquía paralela de métodos delegados. (aunque esto no se ve bien)
static Void switchExpressionDelegate(OrderType e) { handle(e); return null; } static Void switchExpressionDelegate(InvoiceType e) { handle(e); return null; } public static void main(String[] args) { Event event = new OrderType(); Void nullNoop = switch (event.getType()) { case ORDER -> switchExpressionDelegate((OrderType) event); case INVOICE -> switchExpressionDelegate((InvoiceType) event); case PARCELDELIVERY -> switchExpressionDelegate((OrderType) event); // can throw error in an actual implementation }; }Si agregar nuevas clases es una opción, entonces se pueden agregar clases de adaptador
Como lo señala otra respuesta de sambabcde , la mejor opción parece ser usar un Consumidor
public static void main(String[] args) { Event event = new OrderType(); Consumer<Void> nullNoop = switch (event.getType()) { case ORDER -> e -> handle((OrderType) event); case INVOICE -> e -> handle((InvoiceType) event); case PARCELDELIVERY -> e -> handle((OrderType) event); }; nullNoop.accept(null); }¿Qué hay de ejecutable:
Runnable limitOperationRunnable = switch (limitOperation) { case INSERT -> () -> ...; case UPDATE -> () -> ...; case DELETE -> () -> ...; }; limitOperationRunnable.run();