Décompiler les records C# : les membres générés qui les trahissent
Un record est une classe plus un ensemble précis de membres générés par le compilateur — égalité par valeur, une méthode de clonage, un Deconstruct, un PrintMembers/ToString, des setters init-only. Un décompilateur reconnaît cette empreinte et reconstruit le mot-clé record. Voici exactement ce que le compilateur émet pour un record et comment le motif est relu.
Les records C# ressemblent à une fonctionnalité du langage, mais dans l”assemblage compilé il n”y a pas de marqueur « record » — un record est une classe ordinaire avec un ensemble très spécifique de membres générés boulonnés dessus. Cet ensemble est assez distinctif pour être une empreinte, et le reconnaître est la façon dont un décompilateur transforme un tas de méthodes synthétisées en le seul mot record. Parcourir ce que le compilateur émet réellement explique à la fois la reconnaissance et le comportement que vous obtenez gratuitement en en déclarant un.
En quoi s’expanse un record positionnel
Prenez le canonique d”une ligne :
public record Point(int X, int Y);
Cela compile en une classe complète. Décompilé sans reconnaissance de record, c”est à peu près :
public class Point : IEquatable<Point>
{
public int X { get; init; } // init-only, from the primary ctor
public int Y { get; init; }
public Point(int X, int Y) { this.X = X; this.Y = Y; }
protected virtual Type EqualityContract => typeof(Point);
public override bool Equals(object obj) => Equals(obj as Point);
public virtual bool Equals(Point other) =>
other is not null && EqualityContract == other.EqualityContract
&& EqualityComparer<int>.Default.Equals(X, other.X)
&& EqualityComparer<int>.Default.Equals(Y, other.Y);
public override int GetHashCode() => /* combine EqualityContract, X, Y */;
public static bool operator ==(Point a, Point b) => /* ... */;
public static bool operator !=(Point a, Point b) => !(a == b);
protected Point(Point original) { X = original.X; Y = original.Y; } // copy ctor
public virtual Point <Clone>$() => new Point(this); // clone
public override string ToString() { /* uses PrintMembers */ }
protected virtual bool PrintMembers(StringBuilder builder) { /* X = .., Y = .. */ }
public void Deconstruct(out int X, out int Y) { X = this.X; Y = this.Y; }
}
Une ligne de source, une douzaine de membres générés. Chacun d”eux est un comportement que vous écririez sinon à la main, et chacun est un indice.
L’empreinte sur laquelle un décompilateur s’appuie
Un décompilateur ne devine pas à partir de « cette classe a un Equals ». Il cherche l”ensemble spécifique et co-occurrent que seul un record produit :
EqualityContract— une propriétéprotected virtual Typerenvoyant le type. Rien d”autre qu”un record n”émet cela, c”est donc le signal unique le plus fort. (Dans un record scellé ou dérivé, il estprotectedouprivate protecteden conséquence, ce qui renseigne aussi le décompilateur sur l”héritage du record.)<Clone>$— une méthode de clonage avec un nom imprononçable en C# (le$ne peut pas apparaître dans un identifiant de code). Elle existe uniquement pour implémenter l”expressionwith. Sa présence est sans ambiguïté.- Égalité par valeur —
IEquatable<T>.Equals(T), unEquals(object)surchargé,GetHashCode, et les opérateurs==/!=, tous générés par le compilateur et tous cohérents entre eux. PrintMembers+ToString— unprotected virtual bool PrintMembers(StringBuilder)apparié avec unToStringqui l”appelle. Le nom et la signature dePrintMemberssont une convention spécifique aux records.Deconstruct— présent quand le record est positionnel (déclaré avec une liste de paramètres). Ses paramètres out s”alignent sur les paramètres du constructeur primaire, c”est ainsi que le décompilateur sait écrirerecord Point(int X, int Y)plutôt qu”un record avec des propriétés déclarées séparément.
Quand l”ensemble complet est présent et porte les marqueurs de généré-par-compilateur, le décompilateur le réduit à une ligne. Quand seule une partie est là — disons que quelqu”un a écrit à la main un Equals et qu”une méthode en forme de <Clone>$ est absente — il ne fait pas semblant ; il montre une classe. Cette honnêteté compte : le mot-clé record est une affirmation sur une sémantique générée, et un décompilateur ne devrait faire l”affirmation que lorsque la preuve est complète.
Propriétés init-only et le truc du modreq
Les propriétés X et Y ci-dessus sont init, pas set, et cela aussi vit dans l”IL sous une forme reconnaissable. Un setter init-only est un setter ordinaire dont la signature porte un modificateur requis (modreq) référençant System.Runtime.CompilerServices.IsExternalInit :
.method public hidebysig specialname instance void
set_X(int32 'value') ...
// the return type is: void modreq(IsExternalInit)
Ce modreq est tout le mécanisme : le compilateur refuse d”appeler le setter hors d”un initialiseur d”objet parce que les appelants doivent comprendre le modificateur, et les anciens compilateurs qui ne le font pas le rejetteront. Un décompilateur lit le modreq et rend init;. C”est aussi un signal corroborant pour la reconnaissance de record — les paramètres d”un record positionnel deviennent des propriétés init-only, donc un groupe de setters init aux côtés du motif EqualityContract/Deconstruct renforce que ceci est un record positionnel plutôt qu”une classe qui se trouve avoir l”égalité par valeur.
with, record struct et héritage
Trois détails supplémentaires apparaissent dans la forme décompilée. L”expression with compile en un appel à <Clone>$ suivi d”assignations de propriété init-only sur le clone — donc quand vous voyez <Clone>$ invoqué puis quelques appels set_/init, c”est un with dans l”original. record struct suit le même scénario moins la méthode de clonage et la machinerie d”héritage (les structs n”héritent pas), donc l”empreinte est dépourvue d”EqualityContract mais a toujours l”ensemble généré d”égalité par valeur et PrintMembers, et un décompilateur le distingue d”un struct nu par exactement ces membres. L”héritage de records laisse sa propre trace : l”Equals d”un record dérivé compare EqualityContract, son PrintMembers appelle base.PrintMembers, et la méthode de clonage est covariante — tout cela permet au décompilateur de reconstruire la relation : BaseRecord plutôt que de l”aplatir.
Pourquoi le relire
Presque toujours, vous voulez le mot-clé record, pas la douzaine de membres expansés — c”est ce que vous avez écrit et ce que vous voulez dire, et Glass.NET le reconstruit par défaut. Mais l”expansion dessous explique des comportements qui surprennent. L”égalité par valeur est structurelle sur les membres déclarés, c”est pourquoi deux records aux champs égaux sont Equals même en étant des objets distincts — et pourquoi ajouter une propriété de collection mutable vous donne une égalité qui ignore le contenu de la collection, visible aussitôt que vous voyez que l”Equals généré ne compare que les références des membres. L”expression with est un clone superficiel, visible comme l”appel à <Clone>$ qui copie des références, pas une structure profonde. Et le fait qu”EqualityContract fasse partie de l”égalité est exactement pourquoi un record de base et un record dérivé ne sont jamais égaux même avec des champs identiques — les types de contrat diffèrent. Lire les membres générés est la façon dont ces comportements cessent d”être du folklore et deviennent quelque chose que vous pouvez voir. Un record est de la syntaxe ; sa sémantique est un ensemble spécifique de méthodes, et dès que vous savez reconnaître l”ensemble, la vue décompilée et la vue du source racontent la même histoire.
Essayez Nebula.NET
Renforcez votre code .NET en quelques minutes — commencez avec l'édition gratuite.