具有 void 返回类型的 switch 表达式

Posted

技术标签:

【中文标题】具有 void 返回类型的 switch 表达式【英文标题】:Switch expression with void return type 【发布时间】:2021-05-18 03:01:24 【问题描述】:

当 switch 分支调用具有 void 返回类型的方法时,有什么方法可以强制对所有枚举值进行详尽检查?硬编码 yield 只是为了哄编译器要求穷举是相当丑陋的。

这是我当前的模式(句柄方法有 void 返回类型)

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

我想使用表达式的原因是在添加新的枚举值但未处理时会出现编译错误。

【问题讨论】:

你在使用 switch 表达式,而你应该使用 switch 语句。 @NomadMaker 表达式的目的是在添加新的枚举值时得到编译错误。 Consumer 可以附上How to ensure completeness in an enum switch at compile time? 提出的建议。另请参阅访客模式described here。 鉴于您的编辑,如何产生演员 event 然后调用 handle 传递 switch 表达式本身作为参数? 你能重构event 以便将handle() 移到那里吗?然后不需要开关,只需event.handle() 【参考方案1】:

也许会产生EventConsumer,所以你会产生一些有用的东西,权衡是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);

如果您关心性能,请继续

根据有关性能损失的评论,执行基准以比较以下场景:

    使用消费者和句柄是实例方法 使用消费者和句柄是静态方法 不使用消费者和句柄是实例方法 不使用消费者和句柄是静态方法

使用 Consumer 对性能的影响是否很大? 静态和实例handle方法有什么区别吗?

结果是:

# 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/ms

通过观察结果,这4个场景没有太大区别。

使用 Consumer 不会对性能产生重大影响。 静态和实例handle方法的性能差异可以忽略不计。

基准测试通过以下方式执行: CPU:Intel(R) Core(TM) i7-8750H 内存:16G JMH 版本:1.19 虚拟机版本: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 
    

【讨论】:

我认为这个解决方案通过在每次执行期间分配一个 lambda 函数而对性能产生负面影响是错误的吗?如果 switch 表达式位于关键热路径中,则它可能很重要。 @ArborealShark 此特定示例中的所有 lambda 表达式都是非捕获的,因此实例将被记忆并缓存在捕获站点,性能开销为零。 @BrianGoetz 仅当那些 handle 方法是 static 注意Score值的差异都在上报Error的数量级上,所以实际得出的结论是基本一致。或者需要更好的测试设置。 doSomeJob()的返回值没有被使用可能会影响结果(使用JMH的黑洞消耗值)。除此之外,您可以尝试使用不同的参数,例如热身,看看它们是否对结果有影响。如果这一切都没有改变结果,那么可能只是方法没有显着差异。【参考方案2】:

问题的陈述有点“XY问题”;你想要的是整体检查,但你要求它被视为一个表达式,不是因为你想要一个表达式,而是因为你想要表达式引擎附带的整体检查。

添加 switch 表达式留下的“技术债务”之一是 switch 语句 能够选择与 switch 表达式相同的整体检查。对于 switch 语句,我们无法追溯更改这一点——switch 语句一直被允许是部分的——但你是对的,能够进行这种类型检查会很好。正如你所猜测的,将它变成一个空的表情开关是一种到达那里的方法,但它确实很丑,更糟糕的是,不容易被发现。它在我们的列表中,可以找到一种方法来允许您选择返回对 switch 语句进行全面检查。在amber-spec-experts 列表上有关于此的讨论;它与其他几个可能的特性有关,设计讨论仍在进行中。

【讨论】:

【参考方案3】:

如果您在发布主代码之前构建并运行了测试类(例如 JUNIT 测试用例),那么您可以将一个简单的保护函数放入您想要查看的每个枚举的任何现有测试类中:

String checkForEnumChanged(YourEnum guard) 
    return switch (guard) 
        case ORDER -> "OK";
        case INVOICE -> "OK";
        case PAYMENT -> "OK";
    ;

这意味着您可以使您的主应用程序代码不使用yield 0; 样式的开关,并在编辑枚举值时在测试类中获得编译错误。

【讨论】:

但实际上你现在正在产生Strings 并且关于它们未被使用的问题是OP所关心的,单元测试也可以不使用switch表达式也足以保护行为,问题与编译时间有关。 @Naman 此代码不适用于 OP 应用程序,它会导致测试代码中的编译失败,从而提醒 OP 需要处理中使用的非屈服开关中的枚举更改主代码库。【参考方案4】:

添加委托

添加一个委托方法来转发请求并返回一个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);
        ;
    


确切类型

假设handle 方法具有确切的类型,则必须添加委托方法的并行层次结构。 (虽然这看起来不太好)


    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
        ;
    

适配器

如果可以选择添加新类,则可以添加适配器类

以上所有内容看起来都差不多

正如sambabcde 的其他回答所指出的,最好的选择似乎是使用消费者

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

【讨论】:

【参考方案5】:

Runnable 怎么样:

Runnable limitOperationRunnable = switch (limitOperation) 
  case INSERT -> () -> ...;
  case UPDATE -> () -> ...;
  case DELETE -> () -> ...;
;
limitOperationRunnable.run();

【讨论】:

以上是关于具有 void 返回类型的 switch 表达式的主要内容,如果未能解决你的问题,请参考以下文章

c# 8.0 switch 表达式返回类型和空值

建议8:恰当选用if和switch

递归 - 具有 void 函数返回类型的反向 LinkedList

为啥我无法在具有 void 返回类型的异步函数中捕获异常?

多播代理必须具有返回类型void。为什么?

实验八——函数定义及调用总结