miércoles, 10 de agosto de 2011

C# Básico: Objetos y referencias

La verdad es que ahora hacía bastantes meses que no publicaba nada de la serie “C# básico”. En esta serie pongo posts sobre temas básicos del lenguaje. No es un libro por fascículos, ni un tutorial al uso puesto que los posts no tienen orden en concreto y nacen a partir de inquietudes que observo (mayoritariamente en los foros, pero también por correos que recibo). Todos los posts de esta serie los podéis ver aquí.

En el post de hoy quiero hablar de la diferencia entre objetos y referencias ya que observo que no siempre está clara. Gente que entiende los conceptos básicos de herencia parece liarse en este punto. Muchas veces es un tema pasado rápidamente en muchos libros y tutoriales. Y es que, la verdad, es un tema muy sencillo… ;-)

MiClase miObjeto = new MiClase();





¿Qué hace este código? En muchos sitios leerás que lo que hace es crear un objeto de la clase MiClase. Eso es cierto, pero describe lo que hace lo que hay a la derecha del símbolo de asignación. Qué hace el código que está a la izquierda? Pues lo que hace es declarar una referencia de tipo MiClase. Otra palabra que se usa muchas veces en lugar de referencia es variable aunque no son técnicamente lo mismo (hay variables que no son referencias y las referencias pueden asignarse a otros elementos que no llamamos usualmente variables como p.ej. los parámetros a una función).



Las referencias contienen objetos. Yo prefiero decir que las referencias apuntan a objetos (aunque esta palabra parece como “maldita”, sin duda por culpa de los punteros) para que quede claro que un mismo objeto puede estar contenido en (apuntado por) más de una referencia.



El tipo de una referencia



Todas las refencias tienen un tipo. Este tipo es único e inmutable durante toda la vida de la referencia. El tipo de una referencia determina que objetos puede contener dicha referencia. En concreto:




  1. Objetos del mismo tipo. Es decir, una referencia de tipo MiClase puede contener objetos de la clase MiClase.


  2. Objetos de una clase derivada de la clase del tipo de la referencia. Si la referencia es de tipo MiClase puede contener objetos de cualquier clase derivada de MiClase.


  3. Objetos de cualquier clase que implemente el tipo de la referencia. Eso aplica sólo si el tipo de la referencia es una interfaz. En este caso la referencia puede contener un objeto de cualquier clase que implemente dicha interfaz.



Todas las clases en .NET derivan de Object. Por lo tanto, según el punto (2) una referencia de tipo Object, puede contener cualquier objeto de cualquier clase:




Object objeto = new CualquierClase();





¿Condiciona alguna cosa más el tipo de la referencia? Pues sí: el tipo de la referencia condiciona como vemos al objeto contenido en dicha referencia. Es decir, la referencia es como un disfraz para el objeto. Le permite “ocultar su tipo real” y mostrarse como el “tipo de la referencia”.



P.ej. dado el siguiente código:




class MiClase
{
public void Foo() {}
}

class MiClaseDerivada : MiClase
{
public void Bar() {}
}

MiClase c1 = new MiClaseDerivada();
MiClaseDerivada c2 = new MiClaseDerivada();





Podemos ver como MiClase define un método (Foo) y MiClaseDerivada que deriva de MiClase añade el método Bar. Luego c1 es una referencia de tipo MiClase que contiene un objeto de MiClaseDerivada (puede según el punto 2 anterior). Y c2 es una referencia de tipo MiClaseDerivada que contiene un objeto de MiClaseDerivada (posible según el punto 1 anterior). Entonces tenemos que:




c1.Foo();   // Ok.
c1.Bar(); // No compila.
c2.Foo(); // Ok.
c2.Bar(); // Ok.





La llamada c1.Bar() no compila. ¿Por que? Pues simplemente porque la referencia es de tipo MiClase. Y MiClase no tiene ningún método Bar. Da igual que el objeto contenido por dicha referencia sea de tipo MiClaseDerivada, que sí que tiene el método Bar. El compilador no se fija en los tipos de los objetos. Se fija en los tipos de las referencias.



Objetos compartidos



Como hemos dicho antes un mismo objeto puede estar contenido por más de una referencia:




MiClaseDerivada c1 = new MiClaseDerivada();
MiClase c2 = c1;





En este punto tenemos dos referencias. Pero un sólo objeto. Es decir, c1 y c2 contienen el mismo objeto, que es un objeto de tipo MiClaseDerivada. Si accedo al objeto a través de c1 lo veo como un objeto de tipo MiClaseDerivada (ya que este es el tipo de c1). Por otro lado si accedo al objeto a través de c2 lo veo como un objeto de tipo MiClase (al ser este el tipo de c2). Por lo tanto c1.Bar() es correcto y c2.Bar() no compila.



Pero insisto: son el mismo objeto. Observad el siguiente código:




class Program
{
public static void Main()
{
MiClaseDerivada c1 = new MiClaseDerivada();
MiClase c2 = c1;
c1.Incrementar();
c2.Incrementar();
Console.WriteLine("El valor de c1 es:" + c1.Valor);
Console.WriteLine("El valor de c2 es:" + c2.Valor);
}
}


class MiClase
{
private int valor;

public int Valor { get { return this.valor; } }
public void Incrementar()
{
valor++;
}
}

class MiClaseDerivada : MiClase
{
// Código
}





¿Cual es la salida por pantalla de dicho código? Pensadlo con detenimiento. Pues  la siguiente:



El valor de c1 es:2

El valor de c2 es:2



Eso es debido porque c1 y c2 contienen el mismo objeto. Por lo tanto inicialmente tenemos que el valor de dicho objeto es 0. Al llamar a c1.Incrementar() el valor pasa a ser 1. Y al llamar a c2.Incrementar(), el valor pasa a ser 2, ya que el objeto que contiene c2 es el mismo que el objeto que contiene c1.



Así pues recordadlo siempre: Asignar una referencia a otra NO crea un nuevo objeto. Simplemente hace que la referencia contenida a la izquierda de la asignación contenga EL MISMO objeto que la referencia situada a la derecha.



Comparando objetos y referencias.



De nuevo la forma más fácil es verlo con un código de ejemplo:




class Program
{
public static void Main()
{
Persona p1 = new Persona();
p1.Nombre = "Pepito";
p1.Edad = 20;
Persona p2 = p1;
Persona p3 = new Persona();
p3.Nombre = "Pepito";
p3.Edad = 20;
bool b = p2 == p1;
bool b2 = p3 == p2;
}
}

class Persona
{
public string Nombre { get; set; }
public int Edad { get; set; }
}





¿Cual es el valor de b y b2?




  • b vale true porque p1 y p2 contienen el mismo objeto


  • b2 vale false porque p3 y p2 contienen objetos distintos. Da igual que los dos objetos sean del mismo tipo y sean idénticos. En este caso son dos Personas idénticas: mismo nombre y edad. Pero el operador == compara referencias, no objetos.



Así pues recuerda: El operador == al comparar referencias devuelve true sólo si las dos referencias contienen el mismo objeto. En caso contrario devuelve false (aunque las dos referencias apunten a dos objetos idénticos).




Nota: Este comportamiento del operador == puede modificarse para que compare el valor de los objetos en lugar de indicar si las dos referencias contienen el mismo objeto. P.ej. la clase string tienen modificado dicho operador para comparar el valor de las cadenas. Esto queda fuera del alcance de este post.




La comparación de objetos (es decir, determinar si dos objetos son idénticos pese a ser dos objetos distintos) es algo que por norma general depende de la clase. P.ej. dos Personas serán iguales si tienen el mismo nombre y edad. Dos cadenas serán iguales si contienen los mismos carácteres. Depende de cada clase determinar que significa que dos objetos son iguales. Para estandarizar un poco la comparación de objetos, en .NET tenemos el método Equals. Dicho método está definido en la clase Object y por lo tanto, por herencia, existe en todas las clases. Si quiero indicarle al framework como comparar dos objetos de tipo Persona puedo añadir a la clase Persona el siguiente código:




public override bool Equals(object obj)
{
if (obj is Persona)
{
Persona otro = (Persona) obj;
return otro.Edad == Edad &&
otro.Nombre == Nombre;
}
return false;
}



Y para comparar los objetos, debo llamar a Equals en lugar del operador ==




bool b2 = p3.Equals(p2);





Conversiones (castings)



En el código del método Equals anterior hay el siguiente código:




Persona otro = (Persona)obj;





El código (Persona) es lo que se llama casting. El casting lo que hace es cambiar el tipo de una referencia. Es decir en el caso anterior obj era una referencia de tipo object (los parámetros también pueden ser referencias). Recordad que las referencias de tipo object pueden contener cualquier objeto. Pero yo quiero acceder a Nombre y Edad que son campos definidos en la clase Persona y por ello necesito una referencia de tipo Persona que me contenga el mismo objeto que la referencia obj.



Si directamente probáramos:




Persona otro = obj;





Dicho código no compila. ¿Porque? Pues porque otro es una referencia de tipo Persona y por lo tanto solo puede contener:




  1. Un objeto de tipo Persona


  2. Un objeto de cualquier clase que derive de Persona



Pero obj es una referencia de tipo object y puede contener un objeto de tipo object o bien un objeto de cualquier clase que derive de object… es decir, de cualquier clase. Imaginad, entonces:




object obj = new Perro();
Persona otro = obj;





Es evidente que el objeto contenido por obj es un perro y no una persona. Si el código de la segunda línea compilase estaríamos viendo un perro como una persona y bueno… se supone que no se puede, no? Por eso, como el compilador no puede garantizar que el objeto (recordad que el compilador no se fija en objetos) contenido por la referencia obj sea de un tipo válido para la referencia otro, se cura en salud y no nos deja compilar el código.



Pero… tu no eres el compilador y tu sí te fijas en los objetos. ¿Qué pasa en aquellos casos en que tu sabes que el objeto contenido por la referencia obj es de un tipo válido para la referencia Persona? Pues que debes decírselo al compilador. ¿Cómo? Usando el casting:




Persona otro = (Persona)obj;





Aquí le estás diciendo al compilador: Quiero que la referencia otro contenga el mismo objeto que la referencia obj y tranquilo, no te quejes porque yo te digo que el objeto es de tipo Persona. Con el casting el compilador te cree y te deja hacer la asignación.



Eh… que te crea el compilador no significa que te crea el CLR. El CLR no se fía ni de su madre, así que si tu haces:




object perro = new Perro();
Persona persona = (Persona)perro;





El compilador no se quejará, pero cuando ejecutes, vas a recibir una hermosa InvalidCastException. El CLR sí que se fija en los objetos, como tu :)



Ah! Y aunque el compilador no se fije en objetos… no lo insultes, eh? No intentes algo como:




Perro perro = new Perro();
Persona persona = (Persona)perro;





Eso no compila. La razón es porque no es necesario fijarse en los objetos para ver que una referencia de tipo Persona nunca podrá contener el mismo objeto que una referencia de tipo Perro: Persona y Perro no tienen nada en común. El compilador puede no fijarse en los objetos, pero no es tonto!



Un saludo!