C#-Pattern-Matching und switch-Ausdrücke dekompilieren
Ein switch-Ausdruck ist kein Konstrukt in IL — er wird zu einer Kette aus Typprüfungen, Null-Prüfungen und Sprüngen reduziert, die mit einer geworfenen SwitchExpressionException für den nicht abgedeckten Fall endet. Hier steht genau, was der Compiler für Typ-, Eigenschafts-, relationale und logische Muster ausgibt, und wie ein Decompiler die Form wieder zu einem switch zusammensetzt.
Modernes C# stützt sich stark auf Pattern-Matching: Typmuster, Eigenschaftsmuster, and/or/not, relationale Muster und den switch-Ausdruck, der alles verbindet. Nichts davon existiert in IL. Ein switch-Ausdruck kompiliert zu demselben primitiven Kontrollfluss, den eine handgeschriebene if/else-Leiter erzeugen würde, plus einem verräterischen Detail, das der Compiler zur Sicherheit hinzufügt. Die Reduktion durchzugehen erklärt sowohl, was ein Decompiler Ihnen zeigt, als auch, warum die Rekonstruktion manchmal ein Beinahe-Treffer statt eines perfekten Echos der Quelle ist.
Ein switch mit Typmustern
Beginnen wir mit einem switch-Ausdruck über Typen:
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"),
};
Dafür gibt es keine switch-Anweisung. Der Compiler reduziert jeden Zweig zu einer Typprüfung, und der einzige Opcode, der einen Typ prüft, ohne zu werfen, ist isinst: er liefert den zum Ziel typisierten Wert oder null, und der Code verzweigt nach dem Ergebnis:
// shape is Circle c →
ldarg.0
isinst Circle // Circle-oder-null
dup
stloc.0 // c
brtrue.s CIRCLE_ARM // passt → berechne 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
Das ist die Signatur eines Typmusters: isinst unmittelbar gefolgt von einem Null-Prüfungssprung (brtrue/brfalse). castclass erscheint hier nie, denn ein Muster, das nicht passt, muss durchfallen, nicht werfen — isinst liefert genau diese Semantik. Ein Decompiler, der isinst; dup; stloc; brtrue sieht, liest es direkt als is Circle c zurück.
Der throw, der es verrät
Der _-Zweig oben warf explizit, aber sehen Sie, was passiert, wenn ein switch-Ausdruck keinen Platzhalter hat und der Compiler nicht beweisen kann, dass er erschöpfend ist:
public static string Name(DayOfWeek d) => d switch
{
DayOfWeek.Saturday => "weekend",
DayOfWeek.Sunday => "weekend",
_ when d >= 0 => "weekday",
};
Auch hier muss der Compiler garantieren, dass der Ausdruck einen Wert liefert oder deterministisch fehlschlägt, also hängt er einen verborgenen Standardzweig an:
// kein Zweig passte →
ldloc.0 // der geprüfte Wert
newobj instance void [System.Runtime]System.Runtime.CompilerServices.SwitchExpressionException::.ctor(object)
throw
SwitchExpressionException ist praktisch ein Fingerabdruck. Fast kein handgeschriebener Code konstruiert und wirft sie; der Compiler gibt sie nur für einen nicht erschöpfenden switch-Ausdruck aus. Also behandelt ein Decompiler „einen Sprungbaum, der mit einer geworfenen SwitchExpressionException endet” als starken Beleg dafür, dass die Quelle ein switch-Ausdruck war — und stellt ein switch { … } statt einer if/else-Kette dar, was die beiden in der Ausgabe unterscheidet.
Eigenschafts-, relationale und logische Muster
Reichere Muster werden zu mehr vom Gleichen reduziert: Memberlesungen und Vergleiche, kombiniert mit gewöhnlichen booleschen Sprüngen. Ein Eigenschaftsmuster mit einem relationalen und einem logischen Operator:
string Band(Measurement m) => m switch
{
{ Celsius: < 0 } => "freezing",
{ Celsius: >= 0 and < 100 } => "liquid",
{ Celsius: >= 100 } => "boiling",
_ => "unknown",
};
wird zu einer Null-Prüfung auf m, dann Lesungen der Eigenschaft Celsius, verglichen mit den relationalen Operatoren, wobei and zu sequenziellen Sprüngen zusammenfällt, die beide bestehen müssen:
ldloc.0 // m
brfalse UNKNOWN // { ... } erfordert non-null
ldloc.0
callvirt instance float64 Measurement::get_Celsius()
stloc.1 // cacht die Eigenschaftslesung
ldloc.1
ldc.r8 0.0
bge.s CHECK_LIQUID // nicht < 0 → nächsten Zweig versuchen
// → "freezing"
CHECK_LIQUID:
ldloc.1
ldc.r8 0.0
blt.s CHECK_BOILING // < 0 lässt die >= 0-Hälfte scheitern
ldloc.1
ldc.r8 100.0
bge.s CHECK_BOILING
// → "liquid"
Drei Details sind bemerkenswert. Erstens wird die Eigenschaft nur einmal gelesen und in einer temporären Variable gecacht, obwohl drei Zweige sie inspizieren — der Compiler hebt die Lesung heraus. Zweitens ist and kein spezieller Opcode: es sind einfach zwei Vergleiche, die beide bestehen müssen, um den Zweig zu erreichen, und or sind zwei, die jeweils zu ihm springen. Drittens wird not typischerweise zu einer invertierten Sprungbedingung. Ein Decompiler kann diese entweder als das rekonstruierte Muster (>= 0 and < 100) oder als die ausgeschriebenen Vergleiche darstellen; beide sind treu, weshalb die Oberflächensyntax der Ausgabe manchmal von der Quelle abweicht, obwohl das Verhalten identisch ist.
is-Muster außerhalb eines switch
Dieselbe Reduktion treibt ein eigenständiges is an:
if (node is { Parent: not null } n && n.Depth > 3) { … }
Das { Parent: not null } wird zu einer Null-Prüfung auf node, einer Lesung von Parent und einer Null-Prüfung darauf; das deklarierte n ist das in einem Local gespeicherte isinst/cast-Ergebnis; n.Depth > 3 ist ein einfacher Vergleich. Es gibt keine Muster-Metadaten zum Wiederherstellen — ein Decompiler rekonstruiert is { … } aus der Form der Prüfungen, weshalb eine saubere, regelmäßige Reduktion (wie sie der C#-Compiler ausgibt) sich gut rekonstruiert, während stark optimierte oder obfuszierte Sprungbäume als explizite if-Prüfungen zurückkommen können.
Was das beim Lesen von dekompiliertem Code bedeutet
Pattern-Matching ist reiner syntaktischer Zucker über Sprüngen, Typprüfungen und Vergleichen. Drei Fingerabdrücke erlauben einem Decompiler die Rekonstruktion: isinst + Null-Sprung für Typmuster, eine herausgehobene Memberlesung, die relationale Vergleiche speist, für Eigenschafts-/relationale Muster, und ein abschließendes throw new SwitchExpressionException für einen nicht erschöpfenden switch-Ausdruck. Ist die Reduktion regelmäßig, ist die Rekonstruktion sauber; ist sie es nicht, erhalten Sie dennoch korrekten, lesbaren Kontrollfluss — nur ausgedrückt als das if/else, für das der Zucker stand.
Möchten Sie die genaue Reduktion Ihres eigenen Codes sehen? Öffnen Sie den Assembly in Glass.NET, schalten Sie eine beliebige Methode auf die IL-Ansicht und vergleichen Sie sie mit dem C# — der kostenlose Weg, zuzusehen, wie Pattern-Matching zu Sprüngen wird. Siehe auch Records dekompilieren und Lambdas und Closures für weitere Compiler-Fingerabdrücke.
Nebula.NET testen
Härten Sie Ihren .NET-Code in wenigen Minuten — starten Sie mit der kostenlosen Edition.