lunes, 19 de mayo de 2008

C# 3.0 es fácil (i): tipos anónimos

Bueno... con este post empezamos una (pequeña) serie de posts para desgranar las novedades de C# 3.0 (o sea, el C# de VS 2008). Veremos las novedades del lenguaje y como estas se combinan para dar soporte a LINQ... y para empezar, que mejor que tipos anónimos?

Ahora en C# podemos hacer algo como:
var i = 0;
Cuando la gente ve esto, generalmente tiene dos tipos de reacciones:
  1. A los desarrolladores con experiencia en Javascript, o otros lenguajes dinámicamente tipados. se les humedecen los ojos de la emoción de pensar que por fin C# es un lenguaje dinámicamente tipado.
  2. A los desarrolladores con experiencia en VB6 también se les humedecen los ojos, pero en este ocasión es de pánico, al pensar que una cosa parecida a Variant vuelve...
Pues no... ni C# es tipado dinámicamente, ni mucho menos, algo parecido a Variant vuelve para recordarnos nuestras peores pesadillas...
... pues entonces, que significa la palabra clave var?

En C# var significa: declara la variable del tipo correspondiente al tipo de la expresión que se halle a la derecha de la asignación...
O dicho de otro modo:
var i = 0;

es equivalente a:
int i = 0;


Y en este caso equivalente significa lo mismo, exactamente lo mismo.
Si no os lo creeis, probad a compilar esto:
var i = 0;
i = "hola";

Vereis como el compilador se quejará con un claro: Cannot implicitly convert type 'string' to 'int'

Ok, ahora que tenemos claro que significa var, para que lo han añadido al lenguaje? Uno puede pensar que es para evitar que tecleemos tanto... cierto que de var a int no van muchos carácteres, pero de var a Dictionary<MiClase, MiOtroPedazoClase>
, pues ya van unos cuantos... Pero no, nos los tiros no van por ahí... los caminos de Redmond son mucho más elevados. La incorporación de la palabra clave var viene dada por la aparición de los tipos anónimos.
Y que es un tipo anónimo? Pues como su nombre indica, una clase... sin nombre. Y como puede no tener nombre una clase, si necesito su nombre para declararla usando class? Pues porque los tipos anónimos se declaran a la vez que se construye un objeto de este tipo. Algo parecido a los métodos anónimos que los declaramos a la vez que construimos un delegado.
Para declarar un tipo anónimo y construir a la vez un objeto de este tipo de usa la palabra clave new (eso tiene al menos lógica, no? xD).
Observad el siguiente código:

string data = "Francesc Eiximenis";
var eiximenis = new { Nombre = data.Split(' ')[0], Apellido = data.Split(' ')[1] };

Estamos creando un tipo anónimo, con dos propiedades (Nombre y Apellido) y a la vez estamos creando (e inicializando) un objeto de este tipo, y asignándolo a la variable eiximenis. Fijaos en el uso de var para declarar la variable... puesto que si no seria imposible hacerlo (no hay ninguna clase Eiximenis!).
El uso clásico de los tipos anónimos consiste en casos como este: queremos agrupar dos o tres datos en un objeto, pero nos da pereza crear una clase nueva... Pues nada... solucionado!

Pero vamos a hacer cosillas un poco... más divertidas y es que aunque Eiximenis por si sólo ya da para mucho, que pasa si quiero crear pongamos una lista de objetos de este tipo y luego recorrerla? Vamos a verlo...
List<string> _strs = new List<string>();
_strs.Add("Francesc Eiximenis");
_strs.Add("Ramon Llull");
_strs.Add("Bernat Metge");
ArrayList escritores = new ArrayList();
foreach (string s in _strs)
{
string[] tokens = s.Split(' ');
escritores.Add(new { Nombre = tokens[0], Apellido = tokens[1] });
}

Con este código creo tres objetos de mi tipo anónimo y los añado a un ArrayList... Y como se que tengo tres objetos de un tipo anónimo, y no tres objetos de tres tipos anónimos (con las mismas propiedades?)... pues porque el compilador de C# no es tonto del todo y es capaz de reaprovechar tipos anónimos si declaramos dos o más objetos anónimos con las mismas propiedades... cosa que veremos que es realmente interesante.

Ok... pues eso: tengo el ArrayList escritores con tres objetos, y puedo asegurar que los tres son del mismo tipo anónimo... y ahora quiero recorrer este ArrayList...
foreach (var escritor in escritores)
{
Console.WriteLine("Nombre: " + escritor.Nombre);
}

Ajá!!! Listos habreis pensado... pues no :( Este código que acabo de poner... no compila. Y por que? Pues porque var se resuelve en tiempo de compilación, no en tiempo de ejecución... y aunque la ArrayList escritores contenga objetos de mi tipo anónimo, la clase ArrayList define que su enumerador devuelve Object, así que la variable escritor es una referencia de tipo object. Encima como el tipo es anónimo no puedo hacer un cast de escritor al tipo real porque no tiene nombre!
Estamos en un callejón sin salida? Por los pelos... pero no.

La solución pasa por generar una clase que me defina su enumerador al tipo anónimo... en definitiva en lugar de una ArrayList, usar una List<T>, siendo el parámetro genérico T nuestro tipo anónimo...

---- un incómodo silencio va aquí ----

... valeeeeeeeeeeeeee... me habeis pillado!!! Si no tenemos nombre de clase, como vamos a hacer:



List<T> escritores; // Que pongo en lugar de T si no tengo nombre de clase?

Pues por suerte tenemos una posibilidad de hacerlo, aprovechandonos del hecho de que C# es capaz de inferir los tipos genéricos de un método.
Recordad que si tengo el siguiente método:
public static Type TypeOf(T type)
{
return type.GetType();
}

Entonces, gracias a la inferencia de tipos genéricos, estas dos llamadas son equivalentes:
TypeOf<int>(10);
TypeOf(10);


La primera llamada es la tradicional (pasamos el tipo genérico). En la segunda llamada el tipo genérico se infiere a partir del tipo del parámetro.

Entonces si definimos este método:
public static List<T> ListOfType(T type)
{
return new List<T>();
}

Este método, dado un parámetro de un tipo nos devuelve una referencia a una List preparada para contener referncias de este tipo:
List<string> s = ListOfType<string>(string.Empty);

Y aprovechandonos de la inferenncia de tipos genéricos:
List<string> s = ListOfType(string.Empty);

Y si usamos var:
var s = ListOfType(string.Empty);

Todas estas llamadas son equivalentes, y en los tres casos s es una List<string>
. Nos vamos acercando, porque entonces...

var escritores = ListOfType(new { Nombre = string.Empty, Apellido = string.Empty });
foreach (string s in _strs)
{
string[] tokens = s.Split(' ');
escritores.Add(new { Nombre = tokens[0], Apellido = tokens[1] });
}
foreach (var escritor in escritores)
{
Console.WriteLine("Nombre: " + escritor.Nombre);
}

Ya lo tenemos!!!! Fijaos que el truco es declarar la variable escritores, pasándole un objeto de mi tipo anónimo... y eso funciona porque, si os acordáis, os comenté que el compilador de C# reaprovecha el tipo anónimo. Por suerte, porque si cada vez me crease un tipo anónimo distinto esto no funcionaria (el primer Add sobre escritores me petaría con un error de conversión).

Saludos!!!

6 comentarios:

heimar dijo...

Que tal el codigo es lo que yo necesito pero no me funciona dice que el tipo T no existe y el tipo List tampoco me lo reconoce solo me reconoce el tipo List < t donde T debe ser el tipo que quiero por ejemplo string no se si me falta algun using. por otro lado como puedo hacer para obtener los datos del arraylist que tiene tipos anonimos porque por ejmplo si mi variable arraylist se llama ar no puedo obtener esto ar[0].Nombre. Gracias po su colaboracion, este es mi correo por si me puede ayudar hvega@e-gattaca.com

epna dijo...

Hola heimar,

No, no te falta ningún using. El tipo List<T> como tal no existe, es lo que llamamos un genérico: se tiene que especializar T a un tipo concreto. Así tenemos List<string> o List<int> o List<Perro> (si tienes alguna clase Perro, claro).

El problema con los tipos anónimos viene que al no tener nombre, no puedes especializar el tipo genérico T.

La única opción que tienes, es el truco que mencionaba en el post, y usar el método ListOfType que proponía.
Lo declaras como:

public static List<T> ListOfType(T type)
{
return new List<T>();
}

Y luego para usarlo con tipos anónimos:

var lst = ListOfType(new {i=10, str = string.Empty});

Esto te declara una lista que contiene elementos del tipo anónimo que se compone de un entero (i) y una cadena (str).

Sí... lo reconozco, todo un pelín demasiado complicado...

Saludos!

epna dijo...

Ah, heimar, otra cosilla...

Que con una ArrayList, no puedas hacer ar[0].Nombre, se debe a que la ArrayList trabaja con objects. Debes hacer casting antes.
P.ej.

ArrayList ar = new ArrayList();
ar.Add (new Persona());

string s = ((Persona)ar[0]).Nombre;

El problema con los tipos anónimos es el de siempre... como no tienen nombre no puedes hacer cast, así que no puedes usar una ArrayList para almacenarlos...

heimar dijo...

Hola epna gracias por tu comentario,al codigo que me pasaste falta colocarle un T despues del nombre del metodo de esta forma
public static List< T > ListOfType< T >(T type)
{
return new List< T >();
}
Asi funciona correctamente.

Muchas gracias por todo

heimar dijo...

Hola epna tengo el problema que mencionas estoy agrerango tipos anonimos a un arrayList, y no los puedo castear, a que coleccion los podria agregar para que me funcione. este es el codigo que retorna el arraylist

public ArrayList GetEmployeesByPositionMapProduct(DataTable dt, List< string > cargos)
{
var empleados = dt.AsEnumerable();

ArrayList listAux = new ArrayList();
var query = from empleado in empleados
join cargo in cargos on empleado.Field< string >("Position") equals cargo
orderby empleado.Field< string >("Name")
select new { IdEmployee = empleado.Field< string >("IdEmployee"), Name = empleado.Field< string >("Name"), Value = empleado.Field< decimal >("Value") };

foreach (var value in query)
listAux.Add(new { IdEmployee = value.IdEmployee, Name = value.Name, Value = 1,Selected = true });
return listAux;
}
Gracias por tu colaboracion en esto

epna dijo...

Heimar,
Agregando tipos anónimos a un ArrayList, luego no los podrás castear, porque en el foreach los recibes como objects.

La única manera es usar el truco del post (el método ListOfType), pero honestamente es "demasiado" trabajo. La verdad es que los tipos anónimos de C# básicamente existen para dar soporte a LINQ, y NO están pensados para ser "traspasables" entre métodos (por eso se dice que tiene ámbito local).
Si tienes realmente esta necesidad, yo te recomendaría rediseñar tu código para no usar tipos anónimos (recuerda que tienes tambien la creación e inicialización de tipos). Es decir, yo usaria algo como:

class C
{
public int Foo {get;set;}
public string Bar {get;set;}
}

var lst = new List<C>();
lst.Add (new C() {Foo=10, Bar=""});

Saludos!