Decompilar el pattern matching y las expresiones switch de C#
Una expresión switch no es una construcción en IL: se reduce a una cadena de comprobaciones de tipo, comprobaciones de null y saltos, que termina lanzando una SwitchExpressionException para el caso no cubierto. Esto es exactamente lo que el compilador emite para los patrones de tipo, de propiedad, relacionales y lógicos, y cómo un decompilador recupera la forma hasta un switch.
El C# moderno se apoya con fuerza en el pattern matching: patrones de tipo, patrones de propiedad, and/or/not, patrones relacionales y la expresión switch que lo une todo. Nada de eso existe en IL. Una expresión switch se compila al mismo flujo de control primitivo que produciría una escalera if/else escrita a mano, más un detalle revelador que el compilador añade por seguridad. Recorrer la reducción explica tanto lo que un decompilador te muestra como por qué la reconstrucción es a veces un casi-acierto en lugar de un eco perfecto del origen.
Un switch con patrones de tipo
Empieza con una expresión switch sobre tipos:
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"),
};
No hay una instrucción switch para esto. El compilador reduce cada brazo a una comprobación de tipo, y el único opcode que comprueba un tipo sin lanzar es isinst: produce el valor tipado como el destino o null, y el código salta según el resultado:
// shape is Circle c →
ldarg.0
isinst Circle // Circle-o-null
dup
stloc.0 // c
brtrue.s CIRCLE_ARM // coincide → calcula 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
Esa es la firma de un patrón de tipo: isinst seguido inmediatamente de un salto de comprobación de null (brtrue/brfalse). castclass nunca aparece aquí, porque un patrón que no coincide debe caer, no lanzar — isinst da exactamente esa semántica. Un decompilador que ve isinst; dup; stloc; brtrue lo lee directamente como is Circle c.
El throw que lo delata
El brazo _ de arriba lanzaba explícitamente, pero observa qué pasa cuando una expresión switch no tiene un comodín y el compilador no puede demostrar que es exhaustiva:
public static string Name(DayOfWeek d) => d switch
{
DayOfWeek.Saturday => "weekend",
DayOfWeek.Sunday => "weekend",
_ when d >= 0 => "weekday",
};
Incluso aquí el compilador debe garantizar que la expresión produce un valor o falla de forma determinista, así que añade un brazo por defecto oculto:
// ningún brazo coincidió →
ldloc.0 // el valor evaluado
newobj instance void [System.Runtime]System.Runtime.CompilerServices.SwitchExpressionException::.ctor(object)
throw
SwitchExpressionException es prácticamente una huella. Casi ningún código escrito a mano la construye y lanza; el compilador la emite solo para una expresión switch no exhaustiva. Así que un decompilador trata un “árbol de saltos que termina en un SwitchExpressionException lanzado” como fuerte evidencia de que el origen era una expresión switch — y representa un switch { … } en lugar de una cadena if/else, que es lo que distingue a ambas en la salida.
Patrones de propiedad, relacionales y lógicos
Los patrones más ricos se reducen a más de lo mismo: lecturas de miembros y comparaciones, combinadas con saltos booleanos ordinarios. Un patrón de propiedad con un operador relacional y uno lógico:
string Band(Measurement m) => m switch
{
{ Celsius: < 0 } => "freezing",
{ Celsius: >= 0 and < 100 } => "liquid",
{ Celsius: >= 100 } => "boiling",
_ => "unknown",
};
se convierte en una comprobación de null sobre m, luego lecturas de la propiedad Celsius comparadas con los operadores relacionales, con and colapsando a saltos secuenciales que ambos deben pasar:
ldloc.0 // m
brfalse UNKNOWN // { ... } requiere no-null
ldloc.0
callvirt instance float64 Measurement::get_Celsius()
stloc.1 // cachea la lectura de la propiedad
ldloc.1
ldc.r8 0.0
bge.s CHECK_LIQUID // no < 0 → prueba el siguiente brazo
// → "freezing"
CHECK_LIQUID:
ldloc.1
ldc.r8 0.0
blt.s CHECK_BOILING // < 0 falla la mitad >= 0
ldloc.1
ldc.r8 100.0
bge.s CHECK_BOILING
// → "liquid"
Tres detalles valen la pena. Primero, la propiedad se lee una sola vez y se cachea en una temporal aunque tres brazos la inspeccionen — el compilador iza la lectura. Segundo, and no es un opcode especial: son simplemente dos comparaciones que ambas tienen que pasar para alcanzar el brazo, y or son dos que cada una salta a él. Tercero, not normalmente se convierte en una condición de salto invertida. Un decompilador puede representarlos como el patrón reconstruido (>= 0 and < 100) o como las comparaciones expandidas; ambos son fieles, por lo que la sintaxis superficial de la salida a veces difiere del origen aunque el comportamiento sea idéntico.
Patrones is fuera de un switch
La misma reducción impulsa un is autónomo:
if (node is { Parent: not null } n && n.Depth > 3) { … }
El { Parent: not null } se convierte en una comprobación de null sobre node, una lectura de Parent y una comprobación de null sobre eso; el n declarado es el resultado de isinst/cast almacenado en un local; n.Depth > 3 es una comparación simple. No hay metadatos de patrón que recuperar — un decompilador reconstruye is { … } a partir de la forma de las comprobaciones, por lo que una reducción limpia y regular (como la que emite el compilador de C#) se reconstruye bien, mientras que árboles de saltos muy optimizados u ofuscados pueden volver como comprobaciones if explícitas.
Qué significa esto al leer código decompilado
El pattern matching es puro azúcar sintáctico sobre saltos, comprobaciones de tipo y comparaciones. Tres huellas permiten a un decompilador reconstruirlo: isinst + salto de null para los patrones de tipo, una lectura de miembro izada que alimenta comparaciones relacionales para los patrones de propiedad/relacionales, y un throw new SwitchExpressionException final para una expresión switch no exhaustiva. Cuando la reducción es regular, la reconstrucción es limpia; cuando no lo es, aún obtienes un flujo de control correcto y legible — solo que expresado como el if/else que el azúcar representaba.
¿Quieres ver la reducción exacta de tu propio código? Abre el ensamblado en Glass.NET, cambia cualquier método a la vista de IL y compáralo con el C# — la forma gratuita de ver el pattern matching convertirse en saltos. Consulta también decompilar records y lambdas y closures para más huellas del compilador.
Prueba Nebula.NET
Endurece tu código .NET en minutos — empieza con la edición gratuita.