Décompiler le pattern matching et les expressions switch de C#
Une expression switch n'est pas une construction en IL : elle se réduit à une chaîne de tests de type, de tests de null et de branchements, se terminant par une SwitchExpressionException levée pour le cas non couvert. Voici exactement ce que le compilateur émet pour les motifs de type, de propriété, relationnels et logiques, et comment un décompilateur retrouve la forme d'un switch.
Le C# moderne s’appuie fortement sur le pattern matching : motifs de type, motifs de propriété, and/or/not, motifs relationnels, et l’expression switch qui relie le tout. Rien de cela n’existe en IL. Une expression switch se compile vers le même flux de contrôle primitif qu’une échelle if/else écrite à la main, plus un détail révélateur que le compilateur ajoute par sécurité. Parcourir la réduction explique à la fois ce qu’un décompilateur vous montre et pourquoi la reconstruction est parfois un presque-succès plutôt qu’un écho parfait de la source.
Un switch à motifs de type
Commençons par une expression switch sur des types :
public static decimal Price(object shape) => shape switch
{
Circle c => c.Radius * c.Radius * 3.14m,
Rectangle r => r.Width * r.Height,
null => 0m,
_ => throw new ArgumentException("unknown shape"),
};
Il n’y a pas d’instruction switch pour ceci. Le compilateur réduit chaque bras à un test de type, et le seul opcode qui teste un type sans lever est isinst : il fournit la valeur typée vers la cible ou null, et le code branche selon le résultat :
// shape is Circle c →
ldarg.0
isinst Circle // Circle-ou-null
dup
stloc.0 // c
brtrue.s CIRCLE_ARM // correspond → calcule c.Radius * ...
// shape is Rectangle r →
ldarg.0
isinst Rectangle
dup
stloc.1 // r
brtrue.s RECT_ARM
// shape is null →
ldarg.0
brfalse.s NULL_ARM
C’est la signature d’un motif de type : isinst immédiatement suivi d’un branchement de test de null (brtrue/brfalse). castclass n’apparaît jamais ici, car un motif qui ne correspond pas doit retomber, pas lever — isinst donne exactement cette sémantique. Un décompilateur qui voit isinst; dup; stloc; brtrue le relit directement en is Circle c.
Le throw qui le trahit
Le bras _ ci-dessus levait explicitement, mais regardez ce qui se passe quand une expression switch n’a pas de joker et que le compilateur ne peut pas prouver qu’elle est exhaustive :
public static string Name(DayOfWeek d) => d switch
{
DayOfWeek.Saturday => "weekend",
DayOfWeek.Sunday => "weekend",
_ when d >= 0 => "weekday",
};
Même ici le compilateur doit garantir que l’expression produit une valeur ou échoue de façon déterministe, donc il ajoute un bras par défaut caché :
// aucun bras n'a correspondu →
ldloc.0 // la valeur évaluée
newobj instance void [System.Runtime]System.Runtime.CompilerServices.SwitchExpressionException::.ctor(object)
throw
SwitchExpressionException est pratiquement une empreinte. Presque aucun code écrit à la main ne la construit et ne la lève ; le compilateur ne l’émet que pour une expression switch non exhaustive. Un décompilateur traite donc « un arbre de branchements se terminant par un SwitchExpressionException levé » comme une forte preuve que la source était une expression switch — et rend un switch { … } plutôt qu’une chaîne if/else, ce qui distingue les deux dans la sortie.
Motifs de propriété, relationnels et logiques
Les motifs plus riches se réduisent à plus de la même chose : lectures de membres et comparaisons, combinées avec des branchements booléens ordinaires. Un motif de propriété avec un opérateur relationnel et un opérateur logique :
string Band(Measurement m) => m switch
{
{ Celsius: < 0 } => "freezing",
{ Celsius: >= 0 and < 100 } => "liquid",
{ Celsius: >= 100 } => "boiling",
_ => "unknown",
};
devient un test de null sur m, puis des lectures de la propriété Celsius comparées avec les opérateurs relationnels, and se réduisant à des branchements séquentiels qui doivent tous deux passer :
ldloc.0 // m
brfalse UNKNOWN // { ... } exige non-null
ldloc.0
callvirt instance float64 Measurement::get_Celsius()
stloc.1 // met en cache la lecture de la propriété
ldloc.1
ldc.r8 0.0
bge.s CHECK_LIQUID // pas < 0 → essaie le bras suivant
// → "freezing"
CHECK_LIQUID:
ldloc.1
ldc.r8 0.0
blt.s CHECK_BOILING // < 0 échoue à la moitié >= 0
ldloc.1
ldc.r8 100.0
bge.s CHECK_BOILING
// → "liquid"
Trois détails méritent l’attention. D’abord, la propriété est lue une seule fois et mise en cache dans une temporaire même si trois bras l’inspectent — le compilateur hisse la lecture. Ensuite, and n’est pas un opcode spécial : ce sont simplement deux comparaisons qui doivent toutes deux passer pour atteindre le bras, et or deux qui branchent chacune vers lui. Enfin, not devient typiquement une condition de branchement inversée. Un décompilateur peut les rendre soit comme le motif reconstruit (>= 0 and < 100), soit comme les comparaisons développées ; les deux sont fidèles, c’est pourquoi la syntaxe de surface de la sortie diffère parfois de la source même si le comportement est identique.
Motifs is hors d’un switch
La même réduction alimente un is autonome :
if (node is { Parent: not null } n && n.Depth > 3) { … }
Le { Parent: not null } devient un test de null sur node, une lecture de Parent et un test de null sur celle-ci ; le n déclaré est le résultat de isinst/cast stocké dans un local ; n.Depth > 3 est une comparaison simple. Il n’y a aucune métadonnée de motif à récupérer — un décompilateur reconstruit is { … } à partir de la forme des tests, c’est pourquoi une réduction propre et régulière (telle que l’émet le compilateur C#) se reconstruit bien, tandis que des arbres de branchements fortement optimisés ou obfusqués peuvent revenir sous forme de contrôles if explicites.
Ce que cela signifie à la lecture de code décompilé
Le pattern matching est du pur sucre syntaxique sur des branchements, des tests de type et des comparaisons. Trois empreintes permettent à un décompilateur de le reconstruire : isinst + branchement de null pour les motifs de type, une lecture de membre hissée alimentant des comparaisons relationnelles pour les motifs de propriété/relationnels, et un throw new SwitchExpressionException final pour une expression switch non exhaustive. Quand la réduction est régulière, la reconstruction est propre ; quand elle ne l’est pas, vous obtenez tout de même un flux de contrôle correct et lisible — simplement exprimé comme le if/else que le sucre représentait.
Vous voulez voir la réduction exacte de votre propre code ? Ouvrez l’assembly dans Glass.NET, basculez n’importe quelle méthode sur la vue IL et comparez-la au C# — la façon gratuite de voir le pattern matching se transformer en branchements. Voyez aussi décompiler les records et les lambdas et closures pour d’autres empreintes du compilateur.
Essayez Nebula.NET
Renforcez votre code .NET en quelques minutes — commencez avec l'édition gratuite.