Var, object y dynamic

Hola a todos! El otro día me preguntaban sobre las diferencias entre usar var, object y dynamic, y por lo que he podido observar no todo el mundo tiene claro que diferencias hay en cada caso, de ahí que me haya decidido escribir este post.

1. Inferencia de tipos (var)

Para ver el uso de var lo mejor es un ejemplo:

var i = 10;         // Ok
int i2 = i + 1; // Ok
i = "20"; // error CS0029: Cannot implicitly convert type 'string' to 'int'
string s = i; // error CS0029: Cannot implicitly convert type 'int' to 'string'
var j; // error CS0818: Implicitly-typed local variables must be initialized
var k = null; // error CS0815: Cannot assign <null> to an implicitly-typed local variable

La palabra clave var declara una variable cuyo tipo es inferido a partir de la expresión que se le asigna. Así de sencillo. Quizá hay gente que asume que var declara una variable dinámica debido a la influencia de javascript. Pero el var de C# no tiene nada que ver con el var de javascript. P.ej. en C++ se usa la palabra clave auto en lugar de var (de todos modos si buscas información sobre la palabra clave auto en C++ vigila ya que antes tenía otro significado (que casi nadie utilizaba)).

Si analizamos el código anterior vemos que la línea var i=10 declara una variable i cuyo tipo se infiere de la expresión que se le asigna. Dado que la expresión 10 se resuelve a tipo int, la variable i es de tipo int. Podemos ver que podemos asignar i a otra variable de tipo int, pero no podemos asignar i a una variable de tipo string, ni tampoco asignar un string a i (la variable i no es de tipo dinámico y por lo tanto no puede cambiar de tipo).  También podemos observar que no podemos declarar una variable con var sin asignarle expresión (lógico puesto que entonces el compilador no sabe el tipo de dicha variable) y que tampoco la podemos declarar asignándole null (lógico porque null no tiene tipo).

Si os preguntáis para que existe var, no es (sólo) para complacer a los perezosos sino para dar soporte a los tipos anónimos.

2. Dynamic vs object

Visual Studio 2010 viene con la nueva palabra clave dynamic que ahora sí nos permite declarar una variable de tipo dinámico:

dynamic i = 10;         // Ok
int i2 = i + 1; // Ok
i = "20"; // Ok
string s = i; // Ok
dynamic j; // Ok
dynamic k = null; // Ok

Ahora nuestra variable i es de tipo dinámico y es por ello que le podemos asignar un int (como en la primera línea) o bien una cadena (como en la tercera) y del mismo modo podemos asignar la variable i a una cadena. Ojo! Que podamos asignar la variable i a una cadena no significa que sea válido hacerlo: en tiempo de ejecución se realiza la transformación y puede ser que nos de un error. P.ej. el siguiente código compila pero (obviamente) da una excepción en ejecución:

dynamic i = 10;
Guid guid = i;

Si lo probamos vemos que sí, que compila, pero en ejecución nos lanza la excepción RuntimeBinderException con el mensaje Cannot implicitly convert type ‘int’ to ‘System.Guid’.

Veamos ahora más temas interesantes sobre dynamic. Por ejemplo, que creeis que imprime por pantalla el siguiente código:

dynamic i = 10;
Console.WriteLine(i.GetType().FullName);
i = "20";
Console.WriteLine(i.GetType().FullName);

Pues esto:

System.Int32

System.String

Es decir, vemos que aunque la variable i se haya declarado como dynamic, cuando se ejecuta el método GetType() se ejecuta sobre el objeto real contenido por i.

Alguien puede decir que si en el código anterior cambiamos dynamic por object el resultado es idéntico… Entonces… ¿donde está la diferencia? ¿Cuando debo usar dynamic?

Bien, simplificando podemos asumir lo siguiente: En tiempo de ejecución las variables dynamic se traducen a object (el CLR no entiende de dynamic). Pero cuando usamos dynamic el compilador desactiva toda comprobación de tipos, cosa que no ocurre cuando usamos object. Compara los dos códigos:

// Compila
dynamic d = "eiximenis";
string str = d.ToUpper();
// NO compila
object d2 = "eiximenis"; // Ok
string str2 = d2.ToUpper(); // error CS1061: 'object' does not contain a definition for 'ToUpper' and no extension method 'ToUpper' accepting a first argument of type 'object' could be found (are you missing a using directive or an assembly reference?)

El primer código compila mientras que el segundo no, puesto que aunque la variable d2 contiene un objeto de tipo string, la referencia es de tipo object y object no contiene ningún método ToUpper. Mientras que en el caso de dynamic el compilador asume que sabemos lo que estamos haciendo, así que desactiva la comprobación de tipos y listos… Por supuesto si en tiempo de ejecución el objeto referido por la variable dinámica no contiene el método especificado… excepción al canto.

O sea que dynamic no es más que un truco que nos proporciona el compilador: el CLR no sabe nada de dynamic, es el compilador de C# quien hace toda la magia. ¿Quieres otro ejemplo de ello? Aquí lo tienes:

List<dynamic> lst = new List<dynamic>();
List<object> lst2 = new List<object>();
bool b = lst.GetType() == lst2.GetType();
// b vale true

La variable b vale true porque en tiempo de ejecución, tanto lst como lst2 son una List<object>, dado que dynamic se traduce en tiempo de ejecución por object.

3. DLR

Bueno… hemos dicho que cuando usamos dynamic, el compilador lo que hace es básicamente declarar la variable como object y suspender su comprobación de tipos… pero que más hace? Es decir, como traduce:

d.foo();    // d es dynamic

Siendo d una variable declarada como dynamic.

Lo que podría hacer el compilador es simplemente “no traducirlo por nada”, es decir generar el mismo código (IL) como si d fuese una variable tradicional. P.ej. dado el siguiente código C#:

int i = 0;
i.ToString();

El compilador lo traduce en el siguiente código IL (se puede ver con ildasm):

// int i=0;
ldc.i4.0 // Cargamos el valor 0 a la pila
stloc.0 // Sacamos el top de la pila y lo guardamos en la var #0 (i)
// i.ToString();
ldloca.s i // Ponemos la dirección de la variable #0 (i) en la pila
// Llamamos al método ToString. El valor the 'this' se obtiene del top de la pila
call instance string [mscorlib]System.Int32::ToString()

Una opción que tendría el compilador si i estuviese declarada como dynamic en lugar de int seria generar el mismo IL, es decir una llamada tradicional a call. Si en tiempo de ejecución el método indicado no se encuentra en la clase, el CLR da un error.

Otra opción que tiene el compilador es usar Reflection, es decir traducir la llamada d.foo(); a un código parecido a:

// código original es d.foo();
var mi = d.GetType().GetMethods().FirstOrDefault(x => x.Name.Equals("foo"));
object retval = mi.Invoke(d, null);

Este segundo método es más elegante puesto que el compilador podría añadir código propio para gestionar los errores (p.ej. comprobar si mi es null). De hecho el primer método (no traducir nada y generar un IL parecido al que hemos visto) sería muy bestia ya que estamos confiando en la seguridad del CLR y no es esa su función.

Bueno… supongo que si te imaginas que si te estoy metiendo ese rollo es para decirte que el compilador no usa ninguna de esas dos opciones. En su lugar utiliza llamadas al DLR. ¿Y que es el DLR? Pues un conjunto de servicios (construídos encima del CLR) para añadir soporte a lenguajes dinámicos en .NET.

Te puedes preguntar porque necesitamos el DLR y no podemos usar simplemente Reflection. Bien, aunque con Reflection podemos simular llamadas dinámicas, los lenguajes dinámicos permiten más cosas, como p.ej. añadir en tiempo de ejecución métodos a clases o objetos ya existentes. Hacer esto con Reflection es imposible, puesto que Reflection nos permite invocar cualquier miembro de una clase, pero dicho miembro debe estar definido en la clase cuando esta se crea (no se pueden añadir miembros en tiempo de ejecución).

Así pues dado que tenemos al DLR que nos ofrece soporte para tipos dinámicos, el compilador de C# usa llamadas al DLR cuando debe resolver llamadas a miembros de objetos contenidas en variables declaradas como dynamic. Así pues podemos ver que una referencia dynamic se traduce en tiempo de ejecución (gracias al compilador) en una referencia object pero que usará el DLR para acceder a sus miembros.

4. ExpandoObject

Vamos a ver un poco el poder del DLR en acción. Y un ejemplo sencillo y rápido es la clase ExpandoObject.

La clase ExpandoObject representa un objeto al que en tiempo de ejecución se le pueden añadir o quitar propiedades y/o métodos. Fíjate en el siguiente código:

static void Main(string[] args)
{
dynamic eo = new ExpandoObject();
eo.MiPropiedad = 10;
eo.MiOtraPropiedad = "Cadena";
Dump(eo);
Console.ReadLine();
}

static void Dump(dynamic d)
{
Console.WriteLine("MiPropiedad:" + d.MiPropiedad);
Console.WriteLine("MiOtraPropiedad:" + d.MiOtraPropiedad);
}

Creamos un ExpandoObject y luego creamos las propiedades MiPropiedad y MiOtraPropiedad. Crear una propiedad en un ExpandoObject es tan simple como asignarle un valor (ojo! la propiedad sólo se crea cuando se asigna un valor a ella, no cuando se consulta). Luego en el método Dump consultamos dichas propiedades y obtenemos sus valores.

Aquí el uso de dynamic es obligatorio: No podemos declarar la variable eo como ExpandoObject ya que entonces no podemos “añadir propiedades”. Al declarar la variable como dynamic, hacemos que el código compile (el compilador no comprueba que existan las propiedades) y que se use el DLR para llamar a las propiedades MiPropiedad y MiOtraPropiedad. La clase ExpandoObject se integra con el DLR (a través de la interfaz IDynamicMetaObjectProvider) y eso es lo que permite que se añadan esas propiedades al objeto en cuestión.

Resumiendo pues, hemos visto que var simplemente sirve para declarar variables cuyo tipo se infiere de la expresión que se les asigna (necesario para poder asignar un objeto anónimo a una variable) mientras que dynamic es el mecanismo que tenemos en C# para declarar una variable, para la cual el compilador suspenda la comprobación de tipos por un lado y que genere código para usar el DLR por otro.

Saludos!

8 comentarios sobre “Var, object y dynamic”

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *