Codingly

Switch sur un type en C#

Posté en Posts par Romain Verdier à octobre 19, 2009

Si vous avez besoin de faire un switch sur un type en C#, la première chose qu’il vous faut faire est de vous demander si vous avez besoin de faire un switch sur un type. Ensuite, demandez-vous si vous avez vraiment besoin de faire un switch sur un type, et forcez-vous à imaginer une solution tirant partie du polymorphisme (c’est presque toujours possible). Si pour une raison quelconque (imaginons-la tout de même légitime), vous ne pouvez pas faire autrement, alors voici ce qu’il faut éviter de faire :

    switch(expression.GetType().FullName)
    {
        case "System.Linq.Expressions.ConstantExpression":
            VisitConstantExpression((ConstantExpression) expression);
            break;
        case "System.Linq.Expressions.MemberExpression":
            VisitMemberExpression((MemberExpression) expression);
            break;
        case "System.Linq.Expressions.MethodCallExpression":
            VisitMethodCallExpression((MethodCallExpression) expression);
            break;
        case "System.Linq.Expressions.LambdaExpression":
            VisitLambdaExpression((LambdaExpression) expression);
            break;
        default:
            VisitExpression(expression);
            break;
    }

Aussi, si j’étais comme tout le monde (note: je ne suis pas comme tout le monde), je ferais une blague à propos de Dieu qui tue des animaux mignons à chaque fois que vous écrivez quelque chose comme :

    if(expression is ConstantExpression)
        VisitConstantExpression((ConstantExpression)expression);
    else if (expression is MemberExpression)
        VisitMemberExpression((MemberExpression)expression);
    else if (expression is MethodCallExpression)
        VisitMethodCallExpression((MethodCallExpression)expression);
    else if (expression is LambdaExpression)
        VisitLambdaExpression((LambdaExpression)expression);
    else
        VisitExpression(expression);

La bonne solution, même si elle paraitra moins concise aux plus taquins d’entre vous, est la suivante :

    var constantExpression = expression as ConstantExpression;
    if (constantExpression != null)
    {
        VisitConstantExpression(constantExpression);
        return;
    }

    var memberExpression = expression as MemberExpression;
    if (memberExpression != null)
    {
        VisitMemberExpression(memberExpression);
        return;
    }

    var methodCallExpression = expression as MethodCallExpression;
    if (methodCallExpression != null)
    {
        VisitMethodCallExpression(methodCallExpression);
        return;
    }

    var lambdaExpression = expression as LambdaExpression;
    if (lambdaExpression != null)
    {
        VisitLambdaExpression(lambdaExpression);
        return;
    }

    VisitExpression(expression);

Le safe cast évite la redondance is + cast. Rendons tout de même à césar ce qui est aux taquins : c’est un peu verbeux. Heureusement, il est assez facile de factoriser ce genre de code. J’utilise pour ma part une implémentation à base de méthodes d’extensions, qui ressemble à :

    public class Switch
    {
        public object Target { get; set; }

        public Switch(object target)
        {
            this.Target = target;
        }
    }

    public static class SwitchExtensions
    {
        public static Switch Case<T>(this Switch @this, Action<T> action) where T : class
        {
            if (@this == null)
                return null;

            var t = @this.Target as T;
            if (t == null)
                return @this;

            action(t);
            return null;
        }

        public static void Default(this Switch @this, Action action)
        {
            if (@this == null)
                return;

            action();
        }
    }

On retombe alors à l’utilisation sur quelque chose de concis et lisible :

    new Switch(expression)
        .Case<ConstantExpression>(VisitConstantExpression)
        .Case<MemberExpression>(VisitMemberExpression)
        .Case<MethodCallExpression>(VisitMethodCallExpression)
        .Default(() => VisitExpression(expression));

Chaque case prend un délégué en argument. Ici, comme Action<T> matche à chaque fois la signature des méthodes que je veux appeler (Visit*), je passe directement les method groups. Notez qu’il est tout à fait possible de faire autrement :

    new Switch(expression)
        .Case<MemberExpression>(e => Console.WriteLine("MemberExpression : " + e))
        .Case<MethodCallExpression>(e =>
                                    {
                                        Console.WriteLine("Rofl");
                                        VisitMethodCallExpression(e);
                                        Console.WriteLine("Lfor");
                                    });

Enfin, pour ne pas que vous vous sentiez trop lésés par cet articlet, voici en prime une photo de pingouin :

Rhino.Tattoo.dll

Étiqueté :,

21 réponses

Souscrire aux commentaires via RSS.

  1. Gael Fraiteur soumis, le octobre 19, 2009 at 7:11

    Pour completude:

    Dans certains cas, on peut faire un switch sur Type.GetTypeCode(Type). C’est beaucoup plus performant que n’importe quelle autre variante, mais evidemment cela ne fonctionne qu’avec les « intrinseques ».

    http://msdn.microsoft.com/en-us/library/system.typecode.aspx

    • Romain Verdier soumis, le octobre 20, 2009 at 8:05

      Tout à fait. D’ailleurs le switch proposé ne fonctionne qu’avec les reference types.

  2. Laurent Le Brun soumis, le octobre 19, 2009 at 10:33

    Et la meilleure solution est d’utiliser F#. :)

    match x with
    | :? int as i -> printfn « %d » i
    | :? string as s -> printfn « %s » s
    | _ -> printfn « other »

    • Romain Verdier soumis, le octobre 20, 2009 at 8:06

      Héhé, oui, je ne l’ai pas précisé mais faire un switch sur un type est déjà une forme de pattern matching (type matching ?). Du coup, les langages, souvent fonctionnels, qui supportent ces constructions nativement sont avantagés :)

      • Gael Fraiteur soumis, le octobre 20, 2009 at 8:14

        Fonctionnel? Non, logique! Le nec-plus-ultra du pattern matching. Bon vieux Prolog. Pour une fois qu’on avait un langage francais. Il ne merite pas le sort qu’on en a fait.

  3. Simon soumis, le octobre 20, 2009 at 7:43

    Pour le coup, ta syntaxe est jolie (ahh, la mode Fluent…), mais pour le coup, c’est sûrement beaucoup plus lent que le double cast ! Que d’indirections pour remplacer un simple if / else !

    • Romain Verdier soumis, le octobre 20, 2009 at 8:27

      Je ne compte plus le nombre de fois où mes refactorings ont dégradé les performances au profit de la lisibilité et de la factorisation du code. Je ne compte pas m’arrêter aujourd’hui… C’est un peu comme si on se privait de faire un extract method pour ne pas supporter le coup d’un appel supplémentaire :)

      Je veux bien concevoir toutefois que dans ce cas précis tu choisisses de privilégier l’écriture if-ienne ; je trouve pour ma part qu’une telle méthode (soyons fous et imaginons plus de « cases ») est largement candidate pour un refactoring.

      Par curiosité, comment aurais-tu fais ?

      • Simon soumis, le octobre 20, 2009 at 10:12

        Le soucis n’est pas le fait de faire du Refactoring pour favoriser la lisibilité (c’est tout à fait louable si les contraintes de performance sont négligeables), mais plutôt de dire que la solution « is + cast » est mauvaise parce qu’elle fait deux fois la même opération pour rien et que niveau perf c’est pas cool, puis de présenter la solution basée sur la syntaxe Fluent comme étant LE TRUC « hyper hype super cool en vogue dans vos banlieues » qui va régler tous les problèmes. La syntaxe Fluent est plus concise que la version « Safe-Cast », mais en terme de performance elle est encore moins bonne que le double cast (et pas qu’un peu).

        De plus pour le développeur français moyen qui a du mal avec les génériques (faut se mettre au niveau des autres de temps en temps), je suis pas certain que la syntaxe fluent soit si lisible que ça, et du coup, ca me laisse penser que le juste milieu est plus dans le « is + cast » (faudrait faire un sondage sur CodeSource ou autre réseau communautaire pas trop élitiste pour voir, ca pourrait être intéressant).

      • Simon soumis, le octobre 20, 2009 at 10:28

        (sinon dans ce cas précis j’aurais fait un switch(expression.NodeType))

    • Evilz soumis, le octobre 20, 2009 at 10:37

      c’est quoi un générique ? Un médicament ?
      Ca existe en supo ? pour foutre dans le cul de la SNCF … oups je dérappe …

      Bon perso je pense que je partirais sur la 2ème solution is + cast.
      Ca marche, c’est refactorable (no string), c’est pas enorme (en nombre de caractère) et donc c’est lisible.

  4. Jérémy soumis, le octobre 20, 2009 at 11:36

    Je trouve la solution très intéressante, mais je resterai néanmoins (si le cas se présente) sur la solution « if + is + cast ».
    J’ai du mal à voir la lisibilité de la solution proposée puisque pour la comprendre dans sa totalité elle nécessite d’aller dans un autre point du code pour y trouver la définition de la ‘SwitchExtensions’.

  5. Jérémy soumis, le octobre 20, 2009 at 1:23

    Je commence à mieux comprendre l’utilité de ta solution Romain.
    Clairement tout est une question de contexte et de contraintes.
    La simplicité de l’exemple nous fait nous dire que ça fait une peu usine a gaz pour pas grand chose. Mais dans une application plus étendues la méthode d’extension me parait évidemment plus justifié!

    Mais je pense pas qu’on puisse t’en vouloir de faire des exemples simples pour comprendre le cœur du sujet :)

  6. Romain Verdier soumis, le octobre 20, 2009 at 2:34

    @Jérémy, @Vincent : Dire « C’est moins lisible car il faut aller regarder la méthode Case » ça revient à considérer que :

        int candidate = 12345;
        bool result = true;
        if ((candidate & 1) == 0)
        {
            result = candidate == 2;
        }
        else
        {
            int num = (int)Math.Sqrt(candidate);
            for (int i = 3; i <= num; i += 2)
            {
                if ((candidate % i) == 0)
                {
                    result = false;
                    break;
                }
            }
        }
    

    Est plus lisible que :

    bool result = IsPrime(12345);
    

    Car on n’a pas besoin d’aller voir l’implémentation de la méthode IsPrime, ou de savoir que la méthode IsPrime existe.

    • Jérémy soumis, le octobre 28, 2009 at 3:38

      Tu pousses la remarque à son exagération, ton exemple est évident.

      Mais je pense qu’en restant dans un contexte de swicth/case, l’argument de la lisibilité est valable. On parle d’une logique « basique », contrairement à l’exemple de la méthode « IsPrime ».

      De la même manière pour un swicth/case un nombre limité de cas, et avec des opérations relativement simple, le « if-is-cast » sera plus facile à comprendre.

      Cependant, avec la même construction mais un nombre plus conséquent de cas, et des opération plus complexe pour chacun des cas, ta solution prouve toute son utilité.

      Pour moi ça reste une question de contexte dans le code.

  7. Olivier soumis, le octobre 20, 2009 at 4:13

    Personnellement, je n’utilise que « as » pour les raisons évoquées, le is est trop contraignant.

    Sinon, sympa le pattern à la fluent, assez lisible, et surtout typé vs. la solution triviale n° 1, merci de l’exemple, à reprendre je pense, vive les méthodes d’extension ;)

    …bientôt le dynamic, bientôt.

  8. grozeille soumis, le octobre 24, 2009 at 8:27

    J’ai rien compris concernant le pingouin…

  9. Simon Mourier soumis, le novembre 2, 2009 at 3:13

    Oulaaa vous avez dû avoir une enfance torturée pour inventer des trucs pareils :) Mais au moins, c’est assumé!

  10. Romain Verdier soumis, le octobre 20, 2009 at 12:46

    @Simon : A mon sens il faut bien comprendre une chose : Dieu ne tue pas d’animaux pour des raisons de performance. is + cast n’est pas un problème à cause des performances, mais à cause de la redondance impliquée. Tu me fais dire ce que je n’ai pas dit : je n’ai jamais condamné le is + cast pour ses performances, mais bien pour cette redondance qui n’a strictement aucun sens puisqu’il est toujours possible, au moins, d’utiliser as à la place (hors value types).

    L’autre intérêt du switch — et ce n’est pas flagrant dans l’exemple proposé, je vous l’accorde — est qu’il fournit à chaque bloc case, via le paramètre du délégué, une variable du bon type qui peut être utilisée et réutilisée sans cast.

        .Case<MethodCallExpression>(e =>
                                    {
                                        // e est une instance de MethodCallExpression
                                        Console.WriteLine(e.Method);
                                        Console.WriteLine(e.Arguments);
                                        // etc.
                                    });
    

    Dans le cas où l’on doit manipuler n fois l’expression en tant que MethodCallExpression par exemple, l’utilisation répétée du cast devrait choquer tout le monde.

    Quand à la prétendue complexité de cette nouvelle syntaxe, ça me laisse perplexe. On ne doit pas utiliser les lambdas et les generics pour que les membres de CodeS-SourceS puissent nous lire ? Appelez-moi élitiste…

    Edit: Switcher sur le NodeType ne fournit pas une variable manipulable du bon type, il faut encore caster derrière. Mais effectivement lorsqu’il est juste nécessaire de déterminer le type de l’expression, c’est la solution évidente.

  11. Evilz soumis, le octobre 20, 2009 at 1:11

    héhé,
    Perso, j’aime bien ta solution … mais je donne +1 à Jérémy. C’est forcément moins lisible puisqu’il faut connaitre la méthode (d’extention en plus).

  12. loic soumis, le octobre 21, 2009 at 5:36

    A mon humble avis, c’est un peu « over-designé » (excus[é] le quebicisme) __mais__ je donne un point pour l’élégance du code et un point pour parce que Romain est beau.

    Ceci-dit , quit à imaginer des méthodes élegantes, j’aurai je pense tenté de me débarasser du switch et suivre le conseil avisé : « la première chose qu’il vous faut faire est de vous demander si vous avez besoin de faire un switch sur un type[...]« . (on m’objectera qu’en matiere d’ »over design », contourner un switch aboutit parfois a bien pire que la solution présentée :s)

    Ce ceci-dit dit, j’avoue que je ne me serais jamais posé la question de savoir comment j’allais éviter un switch (ou assimilé if..elseif, etc.) par « mon propre switch », c’est vraiement une belle solution.

  13. Pistache soumis, le décembre 11, 2009 at 3:31

    Romain est moche.


Laisser un commentaire