Tipos anónimos: Quién se hace cargo ?

Un tipo anónimo es una clase cuyo nombre es generado por el compilador y que hereda directamente de System.Object. Los miembros de un tipo anónimo son propiedades que son inferidas del inicializador de objeto que crea instancias del tipo en cuestión.
Es posible crear una clase anónima mediante la palabra reservada new y un par de llaves que definen las propiedades públicas y los valores que se desea que la clase contenga. Por ejemplo:

anonymousObject = new { Nombre = «Pedro», Edad = 42 };

La clase anónima anterior contiene 2 propiedades públicas:

  • Nombre (inicializada con la cadena “Juan”)
  • Edad (inicializada con el entero 42)

El compilador infiere los tipos de las propiedades a partir de los valores que se especifican para inicializarlas.

Cuando se define una clase anónima, el compilador genera su propio nombre para la clase, pero no lo revela ni lo hace explícito. Sin embargo en tiempo de ejecución podemos obtener el nombre de la clase generada.

Y aquí surge una paradoja: Si no se conoce el nombre de la clase, cómo es posible crear un objeto(anonymousObject) a partir de esa clase y asignar una instancia(new { Nombre = «Pedro», Edad = 42 }) al mismo ?
La respuesta está en la inferencia de tipos, si se declara a anonymousObject como una variable implícitamente tipada con la palabra reservada var, el compilador creará una variable del mismo tipo que la expresión utilizada para inicializarla. En este caso el tipo de la expresión es el nombre que el compilador genera para la clase anónima. Para que quede claro observemos el código que genera Reflector en relación a la clase anónima generada y a la inferencia de tipos.

La clase anónima que el compilador genera es una clase genérica, si bien Reflector nos muestra una visión un poco confusa, lo correcto sería que hubiese interpretado a la palabra var así:

<>f__AnonymousType0<string, int> anonymousObject = new <>f__AnonymousType0<string, int>(«Pedro», 42);

La clase anónima generada por el compilador es una plantilla, la cual el mismo compilador parametriza, con los tipos que infiere del inicializador de objetos. El nombre que el compilador asignó a la clase anónima es <>f__AnonymousType0 y su definición es la siguiente:

También es posible crear otras instancias y reutilizar la misma clase anónima pero con diferentes valores en sus propiedades.

Al ejecutar el programa, ambas instancias son del mismo tipo:

Reflector también lo confirma, ya que ambos objetos han sido instanciados con los mismos parámetros en la clase genérica.

El compilador C# utiliza los mismos nombres, tipos, número y orden de las propiedades para determinar si 2 instancias de una clase anónima pertenecen al mismo tipo. En este caso como ambas variables tienen el mismo número de propiedades, con el mismo nombre, tipo inferido y orden entonces ambas variables son instancias de la misma clase anónima con los mismos parámetros genéricos.

En síntesis las clases anónimas si tienen un nombre,  ya que el nombre es fundamental en la programación orientada a objetos para identificar y clasificar. Pero ese nombre no lo controla ni lo elige el desarrollador sino el compilador.  Si bien tienen la ventaja de permitirnos crear objetos de una forma flexible(lo cual se aprecia cuando usamos expresiones de consulta) poseen muchas restricciones en su contenido, ya que sólo pueden contener propiedades públicas inicializadas y de sólo lectura.

Ahora les dejo algo para pensar y comentar: 

var anonymousObject = new { Nombre = «Pedro», Edad = 42 };

var anonymousObject2 = new { Nombre = «Juan», Edad = 42f };

if (anonymousObject.GetType() == anonymousObject2.GetType())

Console.WriteLine(«Tipos iguales»);

else

Console.WriteLine(«Tipos distintos»);

 

Teneniendo en cuenta que la única diferencia entre ambos objetos es el tipo de la propiedad Edad, en uno es entero y en otro es flotante:

  • Cuántas clases anónimas generaría el compilador ?
  • Si el compilador genera 2 clases, entonces ambos tipos objetos siempre pertenecerán a tipos diferentes. Pero si genera 1 clase, en qué casos los tipos serían diferentes ?

Fernik

Inferencia de Tipos: Cómo funciona ?

Si hay algo que veo que cuesta que los desarrolladores entiendan o mejor dicho confíen es en la inferencia de tipos que presenta  .net  Framework 3.5. Muchos creen que al usar la palabra var, están  resucitando al costoso tipo Variant de Visual Basic 6.0 o creen que están declarando una variable del tipo Object y que en tiempo de ejecución se resuelve el tipo al que pertenece dicha variable como los hacen los lenguajes dinámicos.


Afortunadamente no es así, la resolución del tipo al que pertenece una variable se resuelve en tiempo de compilación no en tiempo de ejecución.  Vamos a verlo con un ejemplo:



La inferencia de tipos es un proceso por el cual el compilador determina el tipo de una variable local que ha sido declarada sin una declaración explícita de su tipo. El tipo es inferido a partir del valor inicial provisto a la variable. A continuación he declarado una variable llamada “inferredType”, cuyo tipo será resuelto por el compilador C# teniendo en cuenta por supuesto el contenido de la variable. Lo cual revela que para que el algoritmo de inferencia de tipos funcione es necesaria una entrada, que es el contenido de la variable. Si no inicializamos la variable a inferir  tendremos un error de compilación.




La ejecución nos revela que el compilador ha inferido que el contenido de “inferredType” es del tipo System.Int32.



Este resultado sería el mismo que obtendríamos si hubiéramos escrito el siguiente código:



La mejor forma de verificar esto es analizando  el IL que generan ambas implementaciones tanto la inferida como la explícita. Con la ayuda de Reflector el código fuente en C# que se obtiene de analizar el IL de ambos ejecutables es exactamente el mismo.



Ahora bien, analicemos esta situación de error:



Particularmente lo que sucede en este caso, es que el compilador ya infirió que el tipo de la variable “inferredType” es System.Int32. Al asignarle contenido de tipo System.String tenemos un error de conversión de tipos. Me parece importante destacar esta situación ya que conozco a mucha gente que pretende que el compilador analice todas las posibles expresiones en las que interviene la variable y posteriormente generalice al tipo más adecuado para la misma. Pues les tengo malas noticias, el algoritmo de inferencia funciona a partir de la primera evaluación eventual  de una expresión, y esto está bien que sea así ya que la función del compilador es resolver el tipo mediante la reducción de expresiones al tipo atómico más implícito que le permita compilar. En el caso de este ejemplo la expresión analizar es una constante numérica. Si se analizaran todas las expresiones en las que interviene una variable para posteriormente generalizar al mejor tipo, el compilador necesitaría  una instancia privada de SQL Server Analisys Services y tardaría bastante tiempo en generar un ejecutable, lo cual no es la idea.
Ahora si queremos ver el mismo ejemplo con una cadena de texto , el código es el siguiente:


 


Al ejecutarlo obtenemos:



Y cuando analizamos el IL con reflector, conseguimos:



La inferencia en los tipos por valor generaliza al tipo más implícito y optimizado del .net Framework. Como el Framework optimiza la performance para tipos enteros de 32-bit(System.Int32 y System.UInt32) un valor de 0,10 ó 100 que perfectamente podría inferirse como System.Byte se infiere como System.Int32. Incluso se recomienda usar los tipos enteros para contadores(aunque contemos de 0 a 10) y variables enteras de acceso frecuentes, ya que la performance en tiempo de ejecución del tipo entero es preferible al storage en RAM que ahorramos si declaramos variables como System.SByte, System.Byte y System.Int16. De la misma manera, con valores de  punto flotante si declaramos una variable con un valor de 3.14 será inferida al tipo System.Double y no como System.Single(float) que perfectamente la puede contener. La razón es que las operaciones con System.Double son optimizadas por hardware. Sólo se infiere a un tipo no optimizado por el Framework(como System.Int64 ó System.Decimal) si el valor de la variable está fuera del rango de los tipos optimizados.
Si por ejemplo queremos que se infiera el valor 3.14 como float en vez de double, debemos proporcionar cierta evidencia que ayude al compilador a inferirlo como float.


var inferredType= (float)3.14; // casting explícito

var inferredType = 3.14f; // notación sufijo

Entonces, resumiento:




  • La inferencia de tipos no es una solución automágica, el compilador no es psíquico ni el tipo de la variable se resuelve utilizando mecanismos de código dinámico, que afecten la performance en tiempo de ejecución.


  • La inferencia de tipos de resuelve en tiempo de compilación, por lo tanto existe un costo en tiempo de compilación, ese costo  es el tiempo que tarda el algoritmo de inferencia en sintetizar una expresión y resolver el tipo de una variable. 


  • La inferencia de tipos tanto de valor como de referencia es para variables locales de métodos. No se aplica para variables de clase, propiedades, parámetros ni valores de retorno.


  • La inferencia de tipos no es nada más que azúcar sintáctica. Es decir una forma de endulzar y agilizar la manera de declarar variables locales. Realmente se aprecia y se valora  cuando se usa para el desarrollo de pruebas unitarias.


  • El tipo generado por el proceso de inferencia NO es un VARIANT.  Visual Basic  6.0 ha sido discontinuado y es considerado tecnología legacy así que actualícense a Visual Basic.net

Fernik

Estrenando geeks.ms y blog en español

Hola,

Mi nombre es Fernando Hualpa. Soy más conocido como Fernik en Internet y blogs. Antes que nada quiero agradecer a la gente de Plain Concepts por haberme ofrecido un blog en geeks.ms, realmente lo valoro mucho. Mi antiguo blog está en http://fernik.spaces.live.com, generalmente posteaba en inglés, lo cual pienso seguir haciendo  ya que me gusta mucho producir en inglés pero me parece buena idea empezar a generar contenido en español ya que es mi idioma nativo.

Actualmente me desempeño como Microsoft Student Partner en Argentina y como desarrollador en el sector privado, hace 7 años que soy beta tester externo de Microsoft especialmente de Visual Studio, Windows en sus versiones cliente y servidor y SQL Server. Esta actividad me permitió valorar y conocer el proceso de Testing y convertirme en un mejor desarrollador.

Esencialmente creo que es suficiente para empezar, así que sólo me queda decir que si desean contactarse por Live Messenger lo pueden hacer a través de la barra lateral cuando el ícono de presencia esté en verde.

Casi me olvidaba, tengo un par de invitaciones para la beta de Live Mesh, si quieren que los invite por favor comenten esta entrada inicando el email que usan como cuenta passport o Live ID. 

Fernik