Escrito por Eduard Tomás en campusMVP
En este post vamos a ver como distintos lenguajes han abordado la problemática de comparar valores y referencias. O lo que es lo mismo para determinar si dos variables contienen el mismo objeto o contienen dos objetos distintos pero idénticos. Comúnmente a la comparación de referencias se le llama comparación por referencia y cuando se compara si dos objetos son iguales (pero no necesariamente el mismo) se le denomina comparación por valor.
Del mismo modo decimos que un tipo tiene semántica de valor si solo nos importa su valor, pero no su identidad. Si se aplica completamente la semántica de valor asegura la inmutabilidad del objeto: cualquier modificación al objeto genera una copia de dicho objeto, al igual que el operador de asignación.
Es un tema aparentemente sencillo pero que, como casi todo en este apasionante mundo del desarrollo, da para más de lo que parece. Empezaremos con Java...
Java
Java es el lenguaje que más simplifica esta cuestión. Así el operador de igualdad (==) compara referencias. Es decir dado el siguiente código:
MyClass m1 = new MyClass();
MyClass m2 = new MyClass();
boolean eq = m1 == m2;
La variable eq vale false porque m1 y m2 contienen dos objetos distintos. Da igual que estos sean idénticos, lo que importa es que son dos objetos distintos. De hecho uno de los errores más comunes en Java es comparar cadenas utilizando el operador de comparación:
boolean areEqual = "Hola" == "Hola";
En Java las cadenas son un objeto y cada vez que usamos un literal se crea un objeto nuevo, por lo que areEqual valdrá false: tenemos dos cadenas iguales, pero siguen siendo dos objetos. Vale, antes de que corras a Eclipse a copiar este código y ver que areEqual es true (en lugar de false) te diré que esto es debido a que en algunos que casos el compilador de Java puede hacer una optimización y ver que realmente ambas cadenas son iguales y así solo crear realmente un objeto. Pero eso es una optimización del compilador, no del lenguaje. En otros contextos en los que el compilador no puede detectar esto (por ejemplo leyendo esa cadena de un archivo) entonces areEqual valdría false.
Java incorpora un mecanismo para la comparación por valor: el método equals definido en la clase Object(clase base de todas las clases en Java). Dicho método debe ser redefinido en todas aquellas clases que deban proporcionar semántica de comparación por valor. Por lo que el mecanismo correcto para comparar cadenas en Java sería:
boolean areEqual = oneString.equals(anotherString);
Donde oneString y anotherString son variables de tipo String.
En Java los tipos simples (int, boolean, double, ...) no son clases y en este caso siempre se comparan por valor al usar el operador de comparación. Los tipos simples en Java tiene semántica de valor.
C#
Las cosas se ponen mucho más interesantes en C#. C# parte de la misma idea de Java: el operador de comparación (==) compara por referencia, mientras que para comparar por valor existe el método Equals, definido en la clase Object (superclase de todas las clases en C#). Las diferencias con Java son fundamentalmente tres.
Primero C# (y .NET en general) permiten definir tipos con semántica de valor. En C# se utiliza la palabra struct para definir un tipo cuya semántica es por valor. Tan solo un detalle: las structs en C# no son inmutables (aunque mi consejo personal es que si alguna vez las creas, las hagas inmutables). Cuando en C# creas una struct debes proporcionar una implementación del operador ==. Si no lo haces ¡no podrás usar este operador para compararlas!
P. ej. podríamos definir una struct usando el siguiente código:
struct Foo
{
public int Bar { get; set; }
public static bool operator !=(Foo one, Foo other)
{
return !(one == other);
}
public static bool operator ==(Foo one, Foo other)
{
return one.Bar == other.Bar;
}
}
Nota: Si redefinimos el operador de igualdad, C# nos obliga también a redefinir el de no-igualdad (!=).
Segundo C# permite redefinir el operador de comparación (==). Eso implica que, incluso aunque tengamos una clase podemos implementar una semántica de comparación por valor redefiniendo ==. Redefinir el operador == solo deberías hacerlo en el caso que crees una clase cuyos objetos sean inmutables.
Y por último en C# los "tipos simples" realmente no existen: son todo objetos. Todos los "tipos simples" de C# (int, bool, float) son alias para tipos del .NET Framework (p. ej. int es un alias de Int32). Dichos tipos están implementados con semántica de valor (son structs).
Fíjate por lo tanto que en C# las posibilidades se multiplican. Lo normal es tener una clase que al compararla usando == se compare por referencia y al usar Equals(si está redefinido) se compare por valor. Pero podemos tener una clase que se compare con valor al usar == (sin ir más lejos String es una clase y no una struct y tiene el operador == sobrecargado).
Así p. ej. cuando tienes el siguiente código:
var eq = "Hola" == "Hola"
Está comparando dos objetos String , así que esto debería a priori devolver false. Pero la clase Stringtiene el operador == sobrecargado para que compare por valor, por lo que eq vale true. Por su parte el siguiente código:
var eq = 1 == 1;
Está comparando dos objetos Int32. Int32 es una struct por lo que el operador == compara por valor (¡asumiendo que esté bien implementado!). Así que eq vale true de nuevo. Finalmente en este código:
var f1 = new Foo() {Bar=10};
var f2 = new Foo() {Bar=10};
var eq = f1 == f2;
Poca cosa podemos decir. Si Foo es una clase y no tiene el operador == sobrecargado eq valdrá false, ya que se compara por referencia. Por otro lado si Foo es la struct que hemos definido antes, entonces eqdebería devolver true, ya que entonces el operador == compara por valor.
JavaScript
En JavaScript las cosas son mucho más simples: no hay comparación por valor, siempre es por referencia, excepto en los tipos simples (booleanos, números y cadenas). En el resto de casos el operador == indica si dos referencias apuntan al mismo objeto.
Así dado el siguiente código:
var a={};
var b={}
var eq = a==b;
Al final eq valdrá false.
Si queremos comparaciones por valor nos tocará implementarlas manualmente. De hecho en C# y Java debemos hacerlo igualmente (debemos redefinir Equals o equals) pero al menos ambos lenguajes nos dan un punto "donde colocar" el código para comparar por valor (aunque eso tiene el efecto colateral de que cualquier objeto tiene el método Equals o equals incluso cuando estos no estén redefinidos y por lo tanto la comparación por valor no sea correcta).
Pero JavaScript introduce un nuevo operador de comparación adicional. El triple igual (===). Dicho operador es idéntico al doble igual pero con una diferencia fundamental: no realiza coerciones de tipos. La coerción de tipos es la característica que los datos de un determinado de tipo sean convertidos automáticamente a datos de otro tipo. A pesar de lo que mucha gente cree, la coerción de tipos no tiene nada que ver con que el lenguaje sea dinámico (como JavaScript) o estático. De hecho C# es un lenguaje estático, fuertemente tipado y soporta coerciones de tipos. El problema en JavaScript es que las coerciones son muy amplias por lo que las comparaciones que a veces se llevan a cabo con el == devuelven datos aparentemente incorrectos (desde el punto de vista del desarrollador, claro).
Así el código:
1 == "1"
Devuelve true, ya que por la coerción de tipos el "1" es convertido a número. Es la cadena la que se convierte a número porque el tipo de la izquierda es un número (la coerción siempre se aplica del tipo de la derecha al tipo de la izquierda). Por otro lado el siguiente código:
1 + 1 == 1+"1"
Devuelve false. Porque el operador de suma convierte todos los argumentos a cadena, si alguno lo es. Así 1+"1" es equivalente a "1"+"1" lo que vale "11". Luego antes de comparar, la cadena "11" se convierte a número (11) y se compara con el 2 (el resultado del 1+1 de la izquierda).
Para evitar esos resultados a veces "ilógicos" está el operador de triple igual. De hecho deberías usar siempre === antes que == en JavaScript, a no ser que tengas una razón clara para no hacerlo. Dicho operador compara los valores pero deshabilita la coerción de tipos. Así:
1==="1"
Devuelve false, ya que los tipos (numérico por un lado y cadena por el otro no coinciden).
Swift
¿Qué tiene qué añadir Swift al tema de las comparaciones? Pues realmente nada que no hayamos visto ya. Pero su sintaxis puede confundir un poco a quienes vengan de... cualquier otro lenguaje.
La razón es que Swift tiene, al igual que JavaScript, dos operadores de comparación: el doble igual (==) y el triple igual (===). Pero su uso nada tiene que ver con el de JavaScript. Simplemente:
- El operador == realiza una comparación por valor (equivale al método Equals de C# o equals de Java).
- El operador === realiza una comparación por referencia (equivale al operador == de Java y generalmente al operador == de C#).
Cuando creamos una clase propia en Swift debemos proporcionar una implementación del operador == si queremos que se puedan comparar sus objetos por valor: es lo análogo a redefinir el método Equals en C#:
@infix func == (left: Vector2D, right: Vector2D) -> Bool {
return (left.x == right.x) && (left.y == right.y)
}
@infix func != (left: Vector2D, right: Vector2D) -> Bool {
return !(left == right)
}
No obstante Swift, al igual que C#, distingue entre tipos por referencia (clases) y tipos por valor (structs). Cuando se asigna una variable de tipo struct a otra, se copia el objeto entero, por lo que terminamos teniendo dos objetos distintos (al igual que en C#). En este caso el operador === siempre devuelve false cuando se comparan dos variables de tipo struct , a pesar de que contengan dos objetos idénticos.
Y con Swift terminamos este recorrido sobre como distintos lenguajes han implementado algo a priori tan simple como "comparar dos variables" :)
Post original: https://www.campusmvp.es/recursos/post/Comparando-valores-y-referencias-en-varios-lenguajes-de-programacion.aspx
Top comments (0)