Выражение переключателя с возвращаемым типом void

Есть ли способ принудительно выполнить исчерпывающую проверку всех значений перечисления, когда ветви коммутатора вызывают методы с типом возврата 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; }
};

Причина, по которой я хочу использовать выражение, состоит в том, чтобы получить ошибку компиляции, когда новое значение перечисления добавляется и не обрабатывается.


person Arboreal Shark    schedule 15.02.2021    source источник
comment
Вы используете выражение переключения, тогда как вы должны использовать оператор переключения.   -  person NomadMaker    schedule 15.02.2021
comment
@NomadMaker Цель выражения - получить ошибку компиляции при добавлении нового значения перечисления.   -  person Arboreal Shark    schedule 15.02.2021
comment
Consumer может сопровождаться предложениями, сделанными на странице Как обеспечить полноту переключателя перечисления во время компиляции?. См. Также шаблон посетителя, описанный здесь.   -  person Naman    schedule 15.02.2021
comment
С учетом вашего редактирования, как насчет того, чтобы выполнить приведение event, а затем вызвать handle, передав само выражение switch в качестве аргумента?   -  person Federico klez Culloca    schedule 15.02.2021
comment
Можете ли вы провести рефакторинг event так, чтобы handle() был перемещен туда? Тогда никакой переключатель не нужен, только event.handle().   -  person John Bayko    schedule 16.02.2021
comment
В некоторых IDE и других инструментах есть опция выдачи предупреждений для неполных переключателей. В Eclipse это контролируется с помощью Preferences ›Java› Compiler ›Errors / Warnings› Incomplete 'switch' cases on enum.   -  person Lii    schedule 17.02.2021


Ответы (4)


Может быть, вы получите Consumer Event, так что вы получите что-то полезное, компромисс - еще одна строка для 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);

Продолжайте, если вас интересует производительность

На основе комментария относительно снижения производительности выполняется тест для сравнения следующих сценариев:

  1. Использование потребителя и дескриптора - это метод экземпляра
  2. Использование потребителя и дескриптора - статический метод
  3. Не используется потребитель, а дескриптор - это метод экземпляра
  4. Отсутствие потребителя и дескриптора - статический метод

Видеть

  • Сильно ли влияет на производительность использование 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 незначительно.

Тест выполняется с:
ЦП: Intel (R) Core (TM) i7-8750H
Память: 16 ГБ
Версия JMH: 1.19
Версия VM: 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 {
    }
}
person samabcde    schedule 15.02.2021
comment
Я ошибаюсь, полагая, что это решение отрицательно сказывается на производительности, выделяя лямбда-функцию во время каждого выполнения? Если выражение switch находится на критическом горячем пути, это может иметь значение. - person Arboreal Shark; 16.02.2021
comment
@ArborealShark Все лямбды в этом конкретном примере не захватывают, и поэтому экземпляры будут мемоизированы и кэшированы на сайте захвата с нулевыми издержками производительности. - person Brian Goetz; 16.02.2021
comment
@BrianGoetz, только когда эти handle методы static - person Holger; 16.02.2021
comment
Обратите внимание, что различия в значениях Score соответствуют порядку величины сообщенной ошибки, поэтому фактический вывод состоит в том, что они в основном одинаковы. Или что нужна лучшая тестовая установка. - person Holger; 26.02.2021
comment
@Holger, спасибо за ваш комментарий. У вас есть идеи о том, какая необходима лучшая настройка? Это связано с тем, что doSomeJob имеет значение долгое время работы (из-за использования потребителя), а не внешний вид (снижение производительности невелико и скрыто ошибкой)? Или мне попробовать изменить параметр теста? - person samabcde; 26.02.2021
comment
Тот факт, что возвращаемое значение doSomeJob() не используется, может повлиять на результат (используйте черную дыру JMH для поглощения значения). Кроме того, вы можете попробовать другие параметры, например разминка, чтобы увидеть, влияют ли они на результат. Если все это не меняет результатов, возможно, подходы существенно не отличаются. - person Holger; 26.02.2021
comment
@Holger, спасибо, я попробую. - person samabcde; 26.02.2021

Постановка вопроса - это что-то вроде проблемы XY; вам нужна проверка целостности, но вы просите, чтобы она рассматривалась как выражение, не потому, что вам нужно выражение, а потому, что вам нужна проверка целостности, которая идет с капюшоном выражения.

Одним из технических недостатков, оставшихся после добавления выражений переключателя, является возможность для операторов переключателя выбирать ту же проверку целостности, которую получают выражения переключения. Мы не могли задним числом изменить это в отношении операторов switch - операторы switch всегда могли быть частичными, но вы правы, что было бы неплохо иметь возможность получить такую ​​проверку типов. Как вы догадываетесь, превращение его в переключатель выражения пустоты - это один из способов добиться этого, но он действительно уродлив и, что еще хуже, будет нелегко обнаружить. Это в нашем списке, чтобы найти способ, позволяющий вам вернуться к полной проверке для операторов switch. В списке amber-spec-experts были обсуждения по этому поводу; это связано с несколькими другими возможными функциями, и обсуждение дизайна все еще продолжается.

person Brian Goetz    schedule 16.02.2021

Если у вас есть тестовые классы (скажем, тестовые примеры JUNIT), которые вы создаете и запускаете перед выпуском основного кода, вы можете добавить простую функцию защиты в любой существующий тестовый класс для каждого перечисления, которое вы хотите просмотреть:

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

Это означает, что вы можете освободить код основного приложения от стиля переключателя yield 0; и получить ошибку компиляции в тестовых классах при редактировании значений перечисления.

person DuncG    schedule 15.02.2021
comment
но, по сути, вы теперь получаете Strings, и вопрос о том, что они не используются, - это то, что беспокоит OP, также модульный тест без использования выражения переключения также может быть достаточным для защиты поведения, вопрос связан со временем компиляции. - person Naman; 15.02.2021
comment
@Naman Этот код не предназначен для использования в приложении OP, он предназначен для того, чтобы вызвать сбой компиляции в тестовом коде, который предупреждает OP о необходимости обрабатывать изменение перечисления в переключателях без выхода, используемых в основной кодовой базе. - person DuncG; 15.02.2021

Добавить делегата

Добавьте метод делегата для пересылки запроса и возврата типа 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, лучшим вариантом, по-видимому, является использование Consumer

    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);
    }
person Thiyanesh    schedule 20.02.2021