32
votes

Expression de commutation avec le type de retour void

Existe-t-il un moyen de forcer une vérification exhaustive de toutes les valeurs d'énumération lorsque le commutateur branche les méthodes d'appel avec type de retour void? Il est assez laid de coder dur un rendement juste pour amadouer le compilateur pour exiger son exhaustivité.

C'est mon modèle actuel (les méthodes de poignée ont un type de retour vide)

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 raison pour laquelle je souhaite utiliser une expression est d'obtenir une erreur de compilation lorsqu'une nouvelle valeur d'énumération est ajoutée et n'est pas gérée.


6 commentaires

Vous utilisez une expression de commutation où vous devriez utiliser une instruction Switch.


@NomAdmaker Le but de l'expression est d'obtenir une erreur de compilation lorsqu'une nouvelle valeur d'énumération est ajoutée.


Le Consumer pourrait être accompagné des suggestions faites à Comment assurer l'exhaustivité dans un commutateur d'énumération au moment de la compilation? . Voir aussi le modèle des visiteurs décrit ici .


Compte tenu de votre modification, qu'en est-il de donner le casting événement , puis d'appeler manche passer l'expression Switch lui-même comme un argument?


Êtes-vous en mesure de refactor Event pour que manche () y soit déplacé? Alors aucun commutateur n'est nécessaire, juste event.handle () .


Certains IDE et autres outils ont une option pour émettre des avertissements pour les commutateurs non exhaustifs. Dans Eclipse, cela est contrôlé avec Préférences> Java> Compiler> Erreurs / avertissements> Cas «Switch» incomplètes sur Enum .


5 Réponses :


21
votes

peut-être produire un Consumer de event , donc vous donnez quelque chose d'utile, le compromis est une ligne de plus pour Consumer.Accept . xxx

Continuez si vous concernez les performances

sur la base du commentaire concernant la pénalité de performance, une référence est effectuée pour comparer les scénarios suivants:

  1. L'utilisation du consommateur et de la manche est une méthode d'instance
  2. L'utilisation du consommateur et de la poignée est une méthode statique
  3. Ne pas utiliser le consommateur et la manche est la méthode d'instance
  4. Ne pas utiliser le consommateur et la poignée est une méthode statique
  5. pour voir

  • L'utilisation du consommateur a un impact important sur les performances?
  • Y a-t-il une différence pour la méthode statique et instance Handle ?

et le résultat est:

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 {
    }
}

En observant le résultat, il n'y a pas beaucoup de différent entre les 4 scénarios.

  • L'utilisation du consommateur n'a pas d'impact significatif sur les performances.
  • Les performances différentes entre la méthode statique et instance> manche sont négligeables.

La référence est effectuée avec:
CPU: Intel (R) Core (TM) I7-8750H
Mémoire: 16g
Version JMH: 1.19
VERSE VM: JDK 15.0.2

# 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


6 commentaires

Ai-je tort de supposer que cette solution a un impact négatif sur les performances en allouant une fonction lambda pendant chaque exécution? Si l'expression du commutateur est dans un chemin chaud critique, cela peut être significatif.


@Arborealshark Tous les lambdas dans cet exemple particulier sont non capturés, et donc les instances seraient mémorisées et mises en cache sur le site de capture, avec des surcharges de performances zéro.


@Briangoetz uniquement lorsque ces méthodes manchent sont static


Notez que les différences des valeurs de score sont toutes dans l'ordre de grandeur de l'erreur signalée, donc la conclusion réelle est qu'elles sont essentiellement identiques. Ou qu'une meilleure configuration de test est nécessaire.


@Holger, merci pour votre commentaire. Avez-vous une idée de ce qui est nécessaire de "Meilleure configuration"? Est-il dû à la longue durée d'exécution de dosomejob faisant la différence (en raison de l'utilisation du consommateur) et non de l'apparence (la pénalité de performance est petite et cachée par erreur)? Ou je devrais essayer de changer le paramètre de référence?


Le fait que la valeur de retour de dosomeJob () ne soit pas utilisé peut affecter le résultat (utiliser le trou noir de JMH pour consommer la valeur). En plus de cela, vous pouvez essayer avec différents paramètres, par exemple Échauffement, pour voir s'ils ont un impact sur le résultat. Si tout ne change pas les résultats, il peut être simplement le cas que les approches ne diffèrent pas de manière significative.



8
votes

Si vous avez des classes de test (disons les cas de test JUnit) que vous créez et exécutez avant de publier votre code principal, vous pouvez déposer une fonction de garde simple dans n'importe quelle classe de test existante pour chaque énumération que vous souhaitez regarder:

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

Cela signifie que vous pouvez garder votre code d'application principal clair sur le style Rendement 0; Switch et obtenir une erreur de compilation dans les classes de test lorsque les valeurs d'énumération sont modifiées. p>


2 commentaires

Mais effectivement, vous cédez maintenant String et la question sur eux est inutilisée est ce qui concerne l'OP, également un test unitaire peut sans utiliser l'expression de Switch suffise également pour garder le comportement, la question est liée Pour compiler le temps.


@NAMAN Ce code n'est pas destiné à être utilisé dans l'application OP, il est là pour provoquer l'échec de la compilation dans le code de test qui alerte OP de besoin de gérer le changement d'énumération des commutateurs non de rendement utilisés dans la base de code principale.



16
votes

L'énoncé de la question est un peu un "problème xy"; Ce que vous voulez, c'est la vérification de la totalité, mais vous demandez qu'il soit traité comme une expression, non pas parce que vous voulez une expression, mais parce que vous voulez la vérification de la totalité qui vient avec une expression.

L'un des éléments de la "dette technique" laissée à partir de l'ajout d'expressions de commutation est la possibilité de Switch instructions pour opter pour la même vérification de la totalité que les expressions de commutation obtiennent. Nous n'avons pas pu modifier rétroactivement les instructions de commutation - les instructions de commutation ont toujours été autorisées à être partielles - mais vous avez raison de dire qu'il serait bien de pouvoir obtenir ce type de vérification de type. Comme vous le pensez, le transformer en un interrupteur d'expression vide est une façon d'y arriver, mais il est en effet laid, et pire, ne sera pas facilement découvrable. C'est sur notre liste de trouver un moyen de vous permettre de revenir sur la vérification totale des instructions de commutation. Il y a eu des discussions sur la liste Amber-Spec-Experts à ce sujet; Il est lié à plusieurs autres fonctionnalités possibles, et les discussions de conception sont toujours en cours.


0 commentaires

0
votes

Ajoutez un délégué

Ajoutez une méthode de délégué pour transférer la demande et renvoyer un void Type

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

Type exact

En supposant que la méthode manche a un type exact, alors une hiérarchie parallèle des méthodes de délégué doit être ajoutée. (Cela ne semble pas bien cependant)

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

adaptateur

Si l'ajout de nouvelles classes est une option, alors les classes d'adaptateur peuvent être ajoutées

Tout ce qui précède semble autour de

Comme indiqué par une autre réponse par sambabcde , la meilleure option semble être d'utiliser un consommateur

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


0 commentaires

-1
votes

Que diriez-vous de runnable:

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


0 commentaires