Codingly

Switch sur un type en C#

Posted in Posts by Romain Verdier on 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

Publicité
Tagged with: ,

26 Réponses

Subscribe to comments with RSS.

  1. Gael Fraiteur said, on 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 said, on octobre 20, 2009 at 8:05

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

  2. Laurent Le Brun said, on 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 said, on 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 said, on 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 said, on 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 said, on 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 said, on 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).

      • Romain Verdier said, on 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.

      • Evilz said, on 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).

      • loic said, on 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.

      • Pistache said, on décembre 11, 2009 at 3:31

        Romain est moche.

      • Simon said, on octobre 20, 2009 at 10:28

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

    • Evilz said, on 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 said, on 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 said, on 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 said, on 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 said, on 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 said, on 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 said, on octobre 24, 2009 at 8:27

    J’ai rien compris concernant le pingouin…

  9. Simon Mourier said, on 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. il mahboul said, on juin 18, 2010 at 11:49

    utilisez la reflexion pour instancier les objets dans leurs propre type, en c# 4.0 dynamic rafle la mise ;

     if (this.GetType().GetProperties().Count() &gt; 0)
                    foreach (PropertyInfo p in this.GetType().GetProperties())
                    {
                        Object result = p.GetValue(this, null);
    
                        if (result == null)
                        {
                            p.SetValue(this, createNodeInstance(Assembly.GetAssembly(p.PropertyType), p.PropertyType.FullName), null);
                            switch (Assembly.GetAssembly(p.PropertyType).GetName().Name)
                            {
                                case "ModelObject": 
                                    Object newObject = createNodeInstance(Assembly.GetAssembly(p.PropertyType), p.PropertyType.FullName);
                                    p.SetValue(this, newObject, null);
                                   
                                default: break;
                            }
                        }
                    }
    
  11. Jocelyn said, on juillet 16, 2010 at 10:05

    Hello,

    Ayant appris à utiliser yield grâce à un post précédent, j’accorde le plus grand intérêt aux solutions proposées par Romain. :-)

    Toutefois, 2 choses m’échappent dans cette suggestion :

    1°) L’arobase qui préfixe le nom du paramètre @this a-t-il une signification particulière dans le langage, ou bien est-ce simplement un caractère légal comme un autre pour nommer un paramètre, et c’est juste que je l’ignorais ?

    2°) Pourquoi avoir créé une seconde classe SwitchExtensions pour étendre la classe Switch plutôt que de pourvoir directement celle-ci des méthodes Case et Default (et travailler directement avec le « vrai » this) ?

    Merci d’avance pour les éclaircissements ! :-)

    • Romain Verdier said, on juillet 16, 2010 at 10:37

      Le @ est utilisable en C# pour les verbatim strings, mais également pour échapper les mots clés du langage et les utiliser comme identifiers (verbatim identifiers). Dans cet exemple, il me permet d’utiliser this comme nom de paramètre alors qu’il s’agit d’un mot clé. C’est une convention que j’utilise pour identifier visuellement le premier paramètre un peu spécial des méthodes d’extensions. Il va sans dire qu’il est tout à fait possible de le nommer autrement…

      Quant aux méthodes de SwitchExtensions, elles permettent ici de gérer le cas où @this est null, justement. Les appels à la méthode Case sont voués à être chainés, or cette dernière retourne null lorsqu’une correspondance est trouvée pour sortir du switch.

      • Jocelyn said, on juillet 16, 2010 at 3:53

        Effectivement tout s’éclaire, et ce @this est une convention tout à fait intéressante qui méritait d’être mise en valeur.
        Merci beaucoup pour cette réponse si rapide ! :-)

        Dans l’attente de lire le prochain post sicharpien !


Votre commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l’aide de votre compte WordPress.com. Déconnexion /  Changer )

Image Twitter

Vous commentez à l’aide de votre compte Twitter. Déconnexion /  Changer )

Photo Facebook

Vous commentez à l’aide de votre compte Facebook. Déconnexion /  Changer )

Connexion à %s

%d blogueurs aiment cette page :