Decompilar tipos anónimos: la clase genérica generada detrás de new { a, b }
Un tipo anónimo no tiene un nombre que tú hayas escrito, pero es una clase real en los metadatos: una genérica que el compilador sintetiza, llamada algo como <>f__AnonymousType0`2, con una propiedad de solo lectura por miembro, un constructor que las recibe todas y sobrescrituras de Equals, GetHashCode y ToString. El truco que la hace reutilizable es que la clase generada es genérica sobre los tipos de sus propiedades, de modo que dos sitios new { } con los mismos nombres de propiedad en el mismo orden comparten un único tipo de respaldo instanciado con argumentos de tipo distintos. Esta entrada lee la clase generada y el sitio de llamada newobj en IL, muestra dónde viven realmente los nombres de propiedad (en las propiedades del tipo generado, no en los parámetros de tipo <Name>j__TPar), recorre el Equals estructural y el GetHashCode de combinación de hashes que escribe el compilador, y explica cómo Glass.NET reconoce el patrón y lo pliega de vuelta a new { Name = ..., Total = ... } en lugar de dejarte un tipo genérico impronunciable.
Escribe un new { } en C# y creas un objeto sin ningún tipo que puedas nombrar. var row = new { Name = customer.Name, Total = customer.Total }; te da un row del que puedes leer .Name y .Total, pasar a LINQ y comparar por igualdad, pero intenta escribir su tipo y no puedes, porque no existe ninguna palabra clave para él. Esa ausencia es una ilusión del lenguaje fuente, exactamente igual que el hecho de que ?. no tenga opcode en IL. En el ensamblado compilado, el tipo anónimo es una clase perfectamente ordinaria. El compilador simplemente la sintetizó, le dio un nombre que no tienes permiso para pronunciar y la emitió junto a tu código. Decompilar bien un tipo anónimo significa reconocer esa clase sintetizada por lo que es y plegarla de vuelta al new { } que la produjo.
La clase que escribe el compilador
Para new { Name = ..., Total = ... } el compilador de C# emite una clase sellada y genérica (un parámetro de tipo por propiedad) que, decompilada literalmente, tiene este aspecto:
[CompilerGenerated]
[DebuggerDisplay(@"\{ Name = {Name}, Total = {Total} }", Type = "<Anonymous Type>")]
internal sealed class <>f__AnonymousType0<<Name>j__TPar, <Total>j__TPar>
{
private readonly <Name>j__TPar <Name>i__Field;
private readonly <Total>j__TPar <Total>i__Field;
public <Name>j__TPar Name => <Name>i__Field;
public <Total>j__TPar Total => <Total>i__Field;
public <>f__AnonymousType0(<Name>j__TPar Name, <Total>j__TPar Total)
{
<Name>i__Field = Name;
<Total>i__Field = Total;
}
// Equals, GetHashCode, ToString overrides follow — see below.
}
El acento grave seguido de 2 en el nombre real de los metadatos, <>f__AnonymousType0\2, significa dos parámetros de tipo. Ser genérica es toda la razón por la que el tipo es reutilizable: el compilador lo cierra sobre argumentos de tipo concretos en cada sitio de llamada, de modo que todo new { }` con los mismos nombres de propiedad en el mismo orden comparte esta única clase generada.
var a = new { Name = "Acme", Total = 42 }; // <>f__AnonymousType0<string, int>
var b = new { Name = other, Total = count }; // SAME type, same instantiation
var c = new { Name = "x", Total = 1L }; // <>f__AnonymousType0<string, long> — same type, different args
var d = new { Total = 1, Name = "x" }; // DIFFERENT type — order of properties differs
Cambia los nombres, reordénalos o añade una propiedad y el compilador genera un tipo de respaldo distinto (<>f__AnonymousType1, y así sucesivamente). Mismos nombres, mismo orden: un único tipo compartido, por muchos literales que hayas escrito.
El sitio de llamada
Crear un objeto anónimo no tiene nada de exótico en IL: es un newobj del tipo genérico construido, con los valores de las propiedades apilados en orden de declaración:
// var row = new { Name = c.Name, Total = c.Total };
ldarg.0
callvirt instance string Customer::get_Name()
ldarg.0
callvirt instance int32 Customer::get_Total()
newobj instance void class '<>f__AnonymousType0'<string, int32>::.ctor(!0, !1)
stloc.0
El operando de newobj lleva el tipo genérico cerrado y la firma de su constructor (!0, !1): los dos parámetros de tipo, ligados aquí a string e int32. Esa es la firma de un tipo anónimo en el punto de uso: una llamada a constructor que rellena todos los campos de solo lectura de una vez. No hay ruta de setter, porque las propiedades son solo de lectura; un tipo anónimo es inmutable por construcción.
Dónde viven realmente los nombres
El error fácil es suponer que los nombres de propiedad se esconden en los nombres de los parámetros de tipo <Name>j__TPar y <Total>j__TPar, y que por tanto desaparecerían en una compilación release. No es así. Esos nombres de parámetros de tipo son solo pistas; los nombres autoritativos son las filas reales de metadatos Property en la clase generada: get_Name, get_Total y las propiedades que los exponen. Un decompilador lee los nombres directamente de esas filas, y por eso new { Name = ..., Total = ... } se reconstruye con sus miembros intactos incluso sin PDB. Lo que un PDB habría añadido es el nombre de la local en la que guardaste la instancia (row), no la forma del tipo anónimo, que está plenamente presente en los metadatos de todos modos.
Las tres sobrescrituras
Los tipos anónimos tienen igualdad por valor, y el compilador lo hace real con tres sobrescrituras. Equals es estructural: convierte el argumento al mismo tipo genérico cerrado y compara cada campo a través de EqualityComparer<T>.Default.
public override bool Equals(object value)
{
var other = value as <>f__AnonymousType0<<Name>j__TPar, <Total>j__TPar>;
return other != null
&& EqualityComparer<<Name>j__TPar>.Default.Equals(<Name>i__Field, other.<Name>i__Field)
&& EqualityComparer<<Total>j__TPar>.Default.Equals(<Total>i__Field, other.<Total>i__Field);
}
GetHashCode es una combinación de hashes de forma fija: parte de una constante semilla por tipo que el compilador incrusta y va plegando el hash de cada campo con el patrón de multiplicar por un primo que Roslyn usa en todas partes.
public override int GetHashCode()
{
int hash = 339618440; // per-type seed baked in by the compiler
hash = hash * -1521134295 + EqualityComparer<<Name>j__TPar>.Default.GetHashCode(<Name>i__Field);
hash = hash * -1521134295 + EqualityComparer<<Total>j__TPar>.Default.GetHashCode(<Total>i__Field);
return hash;
}
ToString construye { Name = ..., Total = ... } con un StringBuilder, que es también lo que refleja el [DebuggerDisplay] de la clase. Ninguna de estas tres es código que tú escribiste; son la firma del compilador para «esto es un tipo anónimo», y juntas son la mayor parte de aquello en lo que se apoya un decompilador.
Cómo Glass lo pliega de vuelta
Ninguna señal por sí sola es concluyente: una clase genérica sellada es algo ordinario, y [CompilerGenerated] aparece en muchos tipos sintetizados. Glass.NET empareja el conjunto: el prefijo de nombre <>f__AnonymousType, la pareja [CompilerGenerated] y [DebuggerDisplay] cuya cadena de formato deletrea literalmente las propiedades, las propiedades solo de lectura sobre campos readonly, el único constructor con todos los argumentos en orden de declaración y las tres sobrescrituras estructurales en sus formas exactas de compilador. Cuando todo encaja, Glass suprime por completo el tipo sintetizado (nunca ves <>f__AnonymousType0 en la salida) y reescribe cada newobj del mismo como una proyección new { ... }, tomando los nombres de miembro de las propiedades y los valores de los argumentos del constructor en el sitio de llamada.
// What Glass renders from the newobj above — the synthesized type is gone.
var row = new { Name = c.Name, Total = c.Total };
Vale la pena mencionar dos matices. Cuando el valor del inicializador ya coincide con el nombre del miembro (new { c.Name } en lugar de new { Name = c.Name }), el compilador infiere el nombre de la propiedad a partir de la expresión, y Glass restaura la forma corta de inicializador de proyección en lugar de deletrear Name = . Y la sintaxis de consulta de LINQ se apoya en tipos anónimos para las cláusulas let y los joins de varias fuentes, donde el compilador enhebra una cadena de tipos anónimos anidados llamados identificadores transparentes; Glass tiene que desenrollar ese anidamiento para recuperar el let original en lugar de sacar a la superficie la fontanería. En ambos casos el principio es el mismo: el tipo anónimo es una clase real que escribió el compilador, descrita por completo en los metadatos, y el trabajo del decompilador es volverla impronunciable de nuevo: devolverte el new { } con el que empezaste, no la maquinaria que hay debajo.
Prueba Nebula.NET
Endurece tu código .NET en minutos — empieza con la edición gratuita.