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!

Diseñar clases para ser heredadas…

Una de las ventajas de la programación orientada a objetos, es la herencia de clases y el polimorfismo: eso es la capacidad para crear clases derivadas a partir de otras clases y poder usar las clases derivadas en cualquier lugar donde se espere la clase base.

El comportamiento por defecto de C# (y VB.NET) es que cuando creamos una clase, esa se puede exteder, es decir puede crearse una clase derivada. Debemos declarar explicitamente la clase como sellada (sealed) para impedir que alguien cree una clase derivada a partir de la nuestra. Es una buena práctica declarar tantas clases sealed como sea posible (al menos las clases públicas, para las internas no son necesarias tantas precauciones ya que no serán visibles desde fuera de nuestro assembly). Si dejamos una clase sin sellar, debemos ser conscientes de que estamos dando la posibilidad a alguien de que derive de nuestra clase. Eso, obviamente, no tiene nada de malo… pero entonces debemos asegurarnos de que nuestra clase está preparada para que se derive de ella.

Métodos virtuales

Los métodos virtuales definen los puntos de extensión de nuestra clase: es decir la clase derivada sólo puede redefinir (override) los métodos que nosotros hayamos marcado como virtuales en nuestra clase. Ese es uno de los aspectos que más me gustan de C# respecto a Java: en Java no hay el concepto de método virtual (o dicho de otro modo, todos lo son). En C# (y en VB.NET) al tener que marcar explícitamente los métodos que vamos a permitir que las clases derivadas redefinan, nos obliga a tener que pensar en cómo puede extenderse nuestra clase. Si nuestra clase no tiene ningún método virtual… qué sentido tiene dejarla sin sellar? Si nuestra clase no tiene métodos virtuales es que o bien hemos pensado que no tiene sentido que se extienda de ella, o que ni hemos pensado en cómo puede extenderse, y en ambos casos, para evitar problemas, es mejor dejar la clase sellada.

Miembros privados

Los miembros privados sólo son accesibles dentro de la propia clase que los declara. Cuando creamos una clase pensada para que se pueda heredar de ella, debemos tener siempre presente el principio de sustitución de Liskov (LSP). Este principio es, digamos, la base teórica del polimorfismo, y viene a decir que si S es una clase derivada de T, entonces los objetos de tipo T pueden ser reemplazados por objetos de tipo S sin alterar el comportamiento de nuestro sistema.

Si estáis habituados con el polimorfismo, diréis que viene a ser lo mismo… pero de hecho es posible tener polimorfismo sin respetar LSP. El polimorfismo viene dado por el lenguaje: es el lenguaje quien nos deja usar objetos de tipo S donde se esperen objetos de tipo T, pero el lenguaje no nos garantiza que se respete el LSP… eso debemos hacerlo nosotros.

¿Y que tiene que ver ese rollo con los métodos privados? Pues bien… imaginad un método virtual (por lo tanto redefinible desde la clase base), que accede a un método privado, para comprobar por ejemplo una precondición:

public class T {
private int count;
public virtual void Remove() {
if (count <= 0) throw new InvalidOperationException();
}
}

Si alguien deriva de nuestra clase T, y redefine el método Remove no tiene mecanismo para poder comprobar esa precondición. Es decir, incluso aunque nosotros documentemos la precondición quien redefine el método Remove() no tiene manera de poder reproducirla, puesto que no puede acceder a la variable count.

Así, si quieres que quien herede de tus clases pueda respetar el LSP, recuerda de no acceder a miembros privados desde métodos virtuales.

Excepciones

El LSP implica que los métodos redefinidos en una clase derivada, no deben lanzar nuevos tipos de excepciones que los que lanza el mismo método en la clase base (a no ser que esos nuevos tipos de excepciones sean a la vez subtipos de las excepciones lanzadas en el método de la clase base). Es decir, si un método foo() de una clase base T, lanza la excepcion IOException y se deriva de dicha clase T, al redefinir el método foo puede lanzarse la excepción IOException o cualquier derivada de esta, pero no podría lanzar una excepción de tipo ArgumentException p.ej.

Java define la clausula throws que indica que tipo de excepciones puede lanzar un método y no permite que los métodos redefinidos lancen excepciones de cualquier otro tipo al declarado en throws. C# no tiene ningún mecanismo que pueda obligar al cumplimiento de esta norma del LSP, así que sólo nos queda, al menos, documentar las excepciones que lanza cada método. Otra opción es definir métodos protegidos para lanzar todas las excepciones. De esa manera si quien deriva de nuestra clase detecta que debe lanzar la excepción X, puede llamar al método ThrowX. Eso garantiza que todas las excepciones se lanzan de forma coherente.

Code Contracts

Los que seguís mi blog sabréis que he hablado de Code Contracts un par de veces. Si eres de los que piensa que Code Contracts es un nuevo Debug.Assert, cuando quieras quedamos para tomar unas cervecillas y discutimos el asunto 🙂

Para el tema que nos ocupa, Code Contracts es básicamente una bendición. LSP obliga a que una clase derivada:

  • Debe mantener todas las precondiciones de la clase base, sin poder añadir más precondiciones.
  • Debe garantizar todas las postcondiciones de la clase base, sin poder eliminar postcondiciones.
  • Debe preservar todos los invariantes de la clase base.

Si no usamos Code Contracts, el principal problema es que las precondiciones, postcondiciones y invariantes, son codigo normal. Si en mi método virtual Foo() tengo un código que comprueba una precondición determinada, cuando se redefina este método dicho código debe ser escrito otra vez, para volver a comprobar la precondición si queremos mantener el LSP. Code Contracts nos garantiza esto automáticamente:

class T
{
protected bool empty;
public T()
{
empty = true;
}
public virtual void Add()
{
Contract.Requires(empty);
Contract.Ensures(!empty);
empty = false;
}
}

class S : T
{
public override void Add()
{
}
}

En este código cuando llamamos al método Add() de la clase S, se evalúan las precondiciones del método… que están definidas en la clase base.

De esta manera el desarrollador de la clase S, no debe preocuparse de reimplementar todas las precondiciones y postcondiciones de nuevo y puede concentrarse en lo que interesa: la redefinición del método Add().

Nota Técnica: Usar Code Contracts no nos exime de declara la variable empty con la suficiente visibilidad. Es decir, aunque sólo accedamos a empty dentro de la precondición contenida en T.Add(), debemos tener presente que desde el punto de vista de Code Contracts, esta precondición también se ejecutará dentro del método S.Add(). Y eso en Code Contracts es literal: Code Contracts no funciona creando un método “oculto” en la clase T que compruebe las precondiciones, sinó que modifica el IL generado por el compilador, para “copiar y pegar” las precondiciones y postcondiciones en cada método donde sean requeridas. Así, pues es “literalmente” como si las llamadas a Contract estuviesen también en S.Add(). Si declaramos la variable empty como private, el código compila (puesto que para el compilador todo es correcto), pero al ejecutarse se lanzará una excepción indicandonos que desde S.Add() estamos intentando acceder a un miembro sobre el cual no tenemos visibilidad.

Code Contracts no obliga al cumplimiento estricto de LSP, dado que el desarrollador de la clase S puede añadir nuevas precondiciones al método Add:

public override void Add()
{
Contract.Requires(otherPostcondition);
}

De todos modos si el desarrollador de la clase derivada hace esto, ya lo hace bajo su conocimiento y responsabilidad y además Code Contracts emite un warning para que quede claro: Method ‘CC1.S.Add’ overrides ‘CC1.T.Add’, thus cannot add Requires.

Conclusión

Cuando creamos clases, espcialmente clases públicas que formen parte de una API que usen otras personas, debemos tener especial cuidado a la hora de diseñarlas. Debemos prestar especial atención al LSP y tener presente que aunqué cumplir el LSP (aunqué siempre es muy recomendable) no sea siempre obligatorio, sí que puede serlo en otros casos, y nosotros como creadores de la clase base, debemos asegurarnos de tener el cuidado necesario y facilitar la vida al máximo para que quien derive de nuestras clases pueda cumplir el LSP… Y a riesgo de hacerme pesado, insisto que Code Contracts es una bendición.

Un saludo a todos!