La diferencia que lo explica todo#
En C# hay dos familias de tipos:
- Tipos por valor (
struct, y los básicos comoint,float,bool): cuando los asignas o los pasas a un método, se copian. - Tipos por referencia (
class): cuando los asignas o los pasas, compartes la misma instancia (una referencia a ella).
Esta única diferencia explica un montón de comportamientos "raros".
El ejemplo que lo deja claro#
// STRUCT (por valor): se copia
struct PuntoStruct { public int x; }
PuntoStruct a = new PuntoStruct { x = 1 };
PuntoStruct b = a; // COPIA
b.x = 99;
Debug.Log(a.x); // 1 (a no cambió)
// CLASE (por referencia): se comparte
class PuntoClase { public int x; }
PuntoClase c = new PuntoClase { x = 1 };
PuntoClase d = c; // MISMA instancia
d.x = 99;
Debug.Log(c.x); // 99 (¡c también cambió!)Con el struct, a y b son cajas independientes. Con la clase, c y d apuntan a la misma caja.
Comparativa#
struct (valor) | class (referencia) | |
|---|---|---|
| Al copiar/pasar | Se copia entero | Se comparte la referencia |
| Dónde vive (normalmente) | En la pila (stack) | En el montón (heap) |
| Puede ser null | No (salvo nullable) | Sí |
| Herencia | No hereda de otras structs | Sí |
| Coste de crear muchos | Barato, sin basura GC | Genera trabajo para el GC |
| Igualdad por defecto | Por valor (contenido) | Por referencia (misma instancia) |
Por qué Vector3 es un struct#
Vector3, Vector2, Quaternion, Color son structs. Por eso pasa esto, que sorprende a todos:
transform.position.x = 5f; // ❌ ERROR de compilacióntransform.position devuelve una copia del Vector3 (es por valor). Modificar la copia no tendría efecto, así que C# ni te deja. La forma correcta:
Vector3 p = transform.position; // copia
p.x = 5f; // modifica la copia
transform.position = p; // asigna de vuelta"No puedo cambiar transform.position.x directamente" es una de las dudas más repetidas. La razón es exactamente esta: Vector3 es un struct (por valor), y position devuelve una copia. Guarda en una variable, modifica y reasigna.
Cuándo usar struct#
Los struct brillan para datos pequeños, inmutables y numerosos:
- ¿Es un dato pequeño (unos pocos campos) que representa un valor (un punto, un color, un rango)? → struct
- ¿Vas a crear muchísimos y quieres evitar basura para el GC? → struct (bien usado)
- ¿Tiene identidad, se comparte, cambia de estado, o hereda? → class
- ¿Dudas? → class (es lo más común y flexible; los MonoBehaviour y ScriptableObjects son clases)
Rendimiento y la trampa del boxing#
Meter un struct donde se espera un object (o una interfaz sin cuidado) provoca boxing: se crea una copia en el heap, generando basura para el recolector. Ocurre, por ejemplo, al guardar structs en colecciones no genéricas antiguas o al usar ciertas APIs. Con las colecciones genéricas modernas (List<T>) no hay boxing. En bucles calientes, el boxing repetido es una fuente silenciosa de tirones.
Un struct que se puede modificar campo a campo es fuente de bugs (por las copias). La recomendación de la industria es hacer los structs inmutables (asignar todo en el constructor y no cambiarlos). Si necesitas mutar estado con frecuencia, probablemente quieras una clase.
Errores frecuentes#
- Esperar que modificar una copia de un struct afecte al original (
transform.position.x = 5). - Usar struct para algo con identidad y estado que cambia (mejor clase).
- Provocar boxing sin darte cuenta en bucles calientes.
- Comparar clases con
==esperando igualdad por contenido (por defecto es por referencia).
Ponte a prueba#
Resumen#
struct = por valor (se copia); class = por referencia (se comparte). Por eso modificar la copia de un struct (como transform.position.x) no funciona, y por eso dos variables de una clase comparten cambios. Usa struct para datos pequeños e inmutables que creas en masa (como Vector3), y class para todo lo que tenga identidad, estado y herencia. Cuidado con el boxing en bucles calientes.