Structs vs Clases. Tan parecidas, tan distintas

[forCode]

En C# existe el concepto de struct y el concepto de clase. La mayoría de desarrolladores se siente cómodo con el concepto de clase, pero muchos de los que trabajan y desarrollan con C# no terminan de conocer el concepto de struct. Para todos los que deseen conocer dicho concepto, va este post :)

Es posible que en todas tus andaduras con C# no te hayas usado nunca, de forma directa, una struct. La verdad es que la mayoría de desarrolladores solemos usar tan solo clases, pero es importante comprender qué es exactamente una struct y como se comporta como lo hace. En .NET existen dos grandes grupos de tipos. Por un lado los tipos por valor (value types) y por otro lado los tipos por referencia (reference types). Se llaman así precisamente porque esa es su semántica de comportamiento: un value type se comporta como si fuese pasado por valor y un reference type lo hace como si fuese pasado por referencia.

Paso por valor vs paso por referencia

Se dice que un tipo es pasado por valor cuando al asignar una variable de este tipo a otra variable se crea una copia. Los tipos simples (int, long, double,…) son tipos por valor:

int x=10;
int y=a;
x=20;

Al final de este código nadie dudará de que el valor de y sigue siendo 10. Al asignar la variable x a la variable y, el valor de x ha sido copiado a y, así que posteriores modificaciones de x no afectan al valor contenido en y. Por otro lado en los tipos por referencia esto no ocurre:

class Foo {
   public string Bar {get; set;}
}
// En código...
var foo = new Foo();
foo.Bar = "foo_Bar";
var foo2 = foo;
foo2.Bar = "foo2_Bar";

Cuál es el valor de foo.Bar al ejecutar este código? Pues foo.Bar valdrá «foo2_Bar» porque tan solo hay un objeto Foo. Un solo objeto referenciado por dos variables (foo y foo2) de ahí, que a esos tipos se les llame tipos por referencia.

Son tipos por valor los tipos básicos (digamos int, float, char), los enums, los delegados y las estructuras. Son tipos por referencia las clases (como string o Foo).

Estructuras vs Clases

Las estructuras (structs) y las clases superficialmente se parecen mucho: ambas pueden tener métodos públicos y privados. Ambas pueden tener campos y propiedades públicas y privadas. Pero ahí terminan sus parecidos. Porque en el fondo son muy diferentes: las clases son tipos por referencia y las estructuras lo son por valor:

class MyClass {
   public string Name { get; set;}
}

struct MyStruct {
   public string Name { get; set;}
}
// En código...
var c1 = new MyClass();
c1.Name = "c1_Name";
var c2 = c1;
c2.Name = "c2_Name";
var s1 = new MyStruct();
s1.Name = "s1_Name";
var s2 = s1;
s2.Name = "s2_Name";

De nuevo, pregúntate cual es el valor de c1.Name, c2.Name, s1.Name y s2.Name al finalizar este código.
Pues la verdad es que:

  1. c1.Name vale «c2_Name»
  2. c2.Name vale «c2_Name»
  3. s1.Name vale «s1_Name»
  4. s2.Name vale «s2_Name»

¿Sorprendido? Esa es precisamente la diferencia entre un tipo por valor y un tipo por referencia. En el tipo por referencia (la clase) tenemos tan solo un objeto, referenciado por dos variables. La asignación no copia el objeto, la asignación tan solo hace que dos variables (referencias) apunten (o referencien) al mismo objeto. Por otro lado en el tipo por valor la asignación copia el objeto. Del mismo modo que cuando asignamos una variable int a otra sabemos que se ha hecho una copia (y que modificar la primera no implica modificar la segunda).

Comparación de estructuras y clases

En el caso anterior el valor de c1==c2 es true. En tipos por referencia el operador == compara las referencias, no el objeto. Es decir nos dice si dos referencias apuntan al mismo objeto y NO si dos objetos son iguales (pero son dos objetos independientes).

En las estructuras el operador ==  no está definido por defecto. Es decir el código s1==s2 no compila. Para que nos compile debemos decirle al compilador como puede comparar las estructuras. Y eso lo hacemos redefiniendo el método Equals. Para ello modificamos MyStruct:

struct MyStruct
    {
        public string Name { get; set; }
        public override bool Equals(object obj)
        {
            if (!(obj is MyStruct)) return false;
            var other = (MyStruct) obj;
            return other.Name == this.Name;
        }
        public static bool operator ==(MyStruct one, MyStruct two)
        {
            return one.Equals(two);
        }
        public static bool operator !=(MyStruct one, MyStruct two)
        {
            return !one.Equals(two);
        }
    }

Por un lado hemos redefinido Equals y por otro implementado los operadores == y != (si implementamos uno debemos implementar el otro). Al final para comparar MyStructs lo que hacemos es mirar que el valor de Name sea el mismo.

Así ahora podemos tener un código como el siguiente:

var c1 = new MyClass();
            c1.Name = "class";
            var c2 = c1;
            c2.Name = "class";
            var s1 = new MyStruct();
            s1.Name = "struct";
            var s2 = s1;
            s2.Name = "struct";
            var areSameObject = c1 == c2;
            var areStructEquals = s1 == s2;

En este código c1==c2 vale true porque c1 y c2 son dos referencias que apuntan al mismo objeto. Y s1==s2 vale true porque s1 y s2 son iguales (aunque haya dos objetos MyStruct) como hemos visto.

En cambio este código:

var c1 = new MyClass();
            c1.Name = "class";
            var c2 = new MyClass();
            c2.Name = "class";
            var s1 = new MyStruct();
            s1.Name = "struct";
            var s2 = new MyStruct();
            s2.Name = "struct";
            var areSameObject = c1 == c2;
            var areStructEquals = s1 == s2;

c1==c2 vale false, porque aunque c1 y c2 apunten a dos objetos que son iguales, hay dos objetos (uno apuntado por c1 y otro apuntado por c2). Y recuerda que == en tipos por referencia compara las referencias (es decir, mira si apuntan al mismo objeto). Por supuesto s1==s2 vale true, porque aunque hay dos objetos MyStruct, como es un tipo por valor el operador == compara los valores (recuerda de hecho que lo hemos tenido que implementar, para decirle al compilador como comparar esos valores).

¿Y para comparar clases por valor?

Es decir, el operador == en tipos por referencia me dice si dos referencias apuntan al mismo objeto. Pero como puedo saber si dos referencias apuntan a dos objetos que son iguales? Pues por un lado la clase debe haber redefinido el métoodo Equals y por otro debo usar Equals para la comparación. Veamos como puede MyClass redefinir Equals:

class MyClass
    {
        public string Name { get; set; }

        public override bool Equals(object obj)
        {
            if (!(obj is MyClass)) return false;
            var other = (MyClass)obj;
            return other.Name == this.Name;
        }
    }

En este caso consideramos que dos objetos MyClass son iguales si el valor de Name lo es.
Ahora en este punto:

var c1 = new MyClass();
            c1.Name = "class";
            var c2 = new MyClass();
            c2.Name = "class";
            var s1 = new MyStruct();
            s1.Name = "struct";
            var s2 = new MyStruct();
            s2.Name = "struct";
            var areSameObject = c1 == c2;
            var areObjectEquals = c1.Equals(c2);
            var areStructEquals = s1 == s2;

El valor de c1==c2 es false (tenemos dos objetos independientes). El valor de c1.Equals(c2) es true (hay dos objetos que son iguales) y el valor de s1==s2 es true (hay dos objetos que son iguales).
A modo de resumen:

  • En un tipo por valor == compara los valores (mira si dos objetos son iguales)
  • En un tipo por referencia == compara las referencias (mira si dos referencias apuntan al mismo objeto)
  • En un tipo por referencia Equals compara los valores (mira si dos objetos son iguales).
  • En un tipo por valor == y Equals son equivalentes.
  • En un tipo por referencia, si == vale false Equals puede valer true
  • En un tipo por referencia, si Equals vale false, == debe valer false

Hay más puntos a tratar sobre las diferencias entre estructuras y clases y entre tipos por valor y referencia en general, pero esto sería tema de otro post.

Saludos!