Switch sur un type en C#
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 :
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
Tout à fait. D’ailleurs le switch proposé ne fonctionne qu’avec les reference types.
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 »
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 :)
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.
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 !
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 ?
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 : 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é leis
+ cast pour ses performances, mais bien pour cette redondance qui n’a strictement aucun sens puisqu’il est toujours possible, au moins, d’utiliseras
à 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.
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.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).
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.
Romain est moche.
(sinon dans ce cas précis j’aurais fait un switch(expression.NodeType))
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.
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’.
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 :)
@Jérémy, @Vincent : Dire « C’est moins lisible car il faut aller regarder la méthode
Case
» ça revient à considérer que :Est plus lisible que :
Car on n’a pas besoin d’aller voir l’implémentation de la méthode
IsPrime
, ou de savoir que la méthodeIsPrime
existe.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.
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.
J’ai rien compris concernant le pingouin…
Oulaaa vous avez dû avoir une enfance torturée pour inventer des trucs pareils :) Mais au moins, c’est assumé!
utilisez la reflexion pour instancier les objets dans leurs propre type, en c# 4.0 dynamic rafle la mise ;
Haha…
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 ! :-)
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
estnull
, justement. Les appels à la méthodeCase
sont voués à être chainés, or cette dernière retournenull
lorsqu’une correspondance est trouvée pour sortir du switch.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 !