CodeGym /Cursos /C# SELF /Diferencias entre record, class y struct

Diferencias entre record, class y struct

C# SELF
Nivel 19 , Lección 4
Disponible

1. ¿Por qué hicieron falta los record?

En C# (y en .NET en general) durante mucho tiempo los bloques principales eran las clases (class) y las estructuras (struct). Pero cada uno de ellos es incompleto para ciertas tareas. Las clases son de referencia, mutables y se comparan por referencia (salvo raras excepciones). Las estructuras son tipos por valor (se copian al pasar), por defecto se comparan byte a byte y casi siempre son mutables (hasta que aparecieron los readonly struct).

Pero si querías una forma simple y natural de guardar datos, que fuera fácil de comparar por valor, clonar rápido y sin preocuparte de que alguien cambie tu objeto en algún sitio — tocaba inventar mil soluciones caseras o tirar de librerías como ValueTuple o incluso System.Tuple. Pero ninguna de ellas es tan bonita y expresiva como uno quisiera.

Por eso en C# 9 apareció record — un tipo de dato que combina declaración corta, seguridad de inmutabilidad y comportamiento orientado a comparación por valor.

2. Los cuatro tipos en una sola página

class struct record record struct
Categoría Tipo de referencia Tipo por valor Tipo de referencia Tipo por valor
Mutabilidad Por defecto — sí Por defecto — sí Por defecto — no (init) Por defecto — no (init)
Comparación Por referencia (==) Por valor (campos) Por valor (fields/properties) Por valor (fields)
Clonación Sólo manualmente Sólo manualmente Soporte integrado (with) Soporte integrado (with)
Herencia No No
Inmutabilidad Hay que implementarla Hay que implementarla Muy fácil de implementar Muy fácil de implementar
Sintaxis La más larga Corta La más corta (posicional) Bastante corta
Uso en colecciones Por referencia Copias Por referencia Copias

Esquema visual

+----------------+     +----------------+     +--------------------+
|    class       |     |    struct      |     |      record        |
+----------------+     +----------------+     +--------------------+
| Reference Type |     | Value Type     |     | Reference Type     |
| Mutable        |     | Mutable        |     | Immutable (init)   |
| == : Reference |     | == : By Fields |     | == : By Value      |
+----------------+     +----------------+     +--------------------+

3. Lo esencial entre record, class y struct

Comportamiento en memoria: ¿referencia o valor?

  • class y record — son tipos de referencia. Cuando los pasas a una función, se copia la referencia al objeto.
  • struct y record struct — son tipos por valor. Siempre se copian byte a byte (a menos que los pases explícitamente por referencia).

class PointClass { public int X; public int Y; }
struct PointStruct { public int X; public int Y; }
record PointRecord(int X, int Y);
record struct PointRecordStruct(int X, int Y);

void Demo()
{
    var pc = new PointClass { X = 1, Y = 2 };
    var ps = new PointStruct { X = 1, Y = 2 };
    var pr = new PointRecord(1, 2);
    var prs = new PointRecordStruct(1, 2);

    ChangeY(pc);    // ¡pc.Y cambiará!
    ChangeY(ps);    // ps.Y no cambiará — ¡es una copia!
    ChangeY(pr);    // ¡pr.Y cambiará!
    ChangeY(prs);   // prs.Y no cambiará — ¡es una copia!
}

void ChangeY(dynamic p) { p.Y = 99; }

Si te parece que dynamic aquí es magia — sí, es sólo para el ejemplo, para que veas que al trabajar con struct el valor no cambia, pero con class o record (de referencia) — sí cambia.

Comparación: ¿cómo saber si dos objetos son iguales?

  • class: se comparan por referencia (== — true sólo si es el mismo objeto en memoria), salvo que sobreescribas Equals.
  • struct: se comparan por valor de todos los campos (por defecto).
  • record: se comparan por valor de todos los campos/propiedades definidos en el constructor primario.
class Foo { public int A; public int B; }
record Bar(int A, int B);

var foo1 = new Foo { A = 42, B = 1 };
var foo2 = new Foo { A = 42, B = 1 };
var bar1 = new Bar(42, 1);
var bar2 = new Bar(42, 1);

Console.WriteLine(foo1 == foo2); // ¡False! Objetos diferentes
Console.WriteLine(bar1 == bar2); // ¡True! Los valores coinciden

Dato curioso:
Con record struct es aún más guay: la comparación es "por valor", como en los struct normales, pero con la sintaxis y ventajas de record.

4. Inmutabilidad: ¿quién garantiza qué?

Comparamos la seguridad de los objetos:

  • class: Por defecto es fácilmente mutable, salvo que pongas todos los campos como readonly.
  • struct: Igual, pero puedes declararlo como readonly struct, entonces ni campos ni propiedades se pueden cambiar.
  • record: Normalmente los campos se declaran con el modificador init, o sea, sólo puedes asignarlos en el constructor o en el inicializador del objeto (with). Es cómodo para pasar datos de forma segura.
  • record struct: Igual, puedes hacer readonly record struct y tener inmutabilidad en tipos por valor con los extras de record.

record Person(string Name, int Age);

var p1 = new Person("Aleksei", 23);
// p1.Age = 24; // ¡Error! Sólo init

var p2 = p1 with { Age = 24 }; // ¡Funciona! Crea una copia con nuevos datos

Cuando tienes un proyecto grande y decenas de entidades, record te ayuda a evitar un montón de bugs del tipo "alguien cambió un campo en algún sitio — y todo se rompió".

5. Sintaxis: cómo se declaran y cómo no liarse


// class
public class Product
{
    public int Id { get; init; }
    public string Name { get; init; }
}

// struct
public struct Point
{
    public int X { get; set; }
    public int Y { get; set; }
}

// record
public record Product(int Id, string Name);

// record struct
public record struct Point(int X, int Y);
Comparación de la sintaxis de declaración de los diferentes tipos

Como ves, la declaración más corta es la del tipo record. Obtienes constructor, destructor, igualdad, ToString() y mucho más "de serie".

Sintaxis Lo que obtienes gratis ¿Se puede heredar?
class nada (casi)
struct nada (casi) No
record ToString, Equals, Deconstruct, with
record struct ToString, Equals, Deconstruct, with No

6. Clonación y el operador with

Sólo record y record struct tienen el operador especial with, que te permite copiar un objeto cambiando sólo algunas propiedades, sin dolor.


record User(string Name, int Age);

var user1 = new User("Irina", 28);
var user2 = user1 with { Age = 29 }; // user2.Name == "Irina", user2.Age == 29

En el caso de una clase (class) tendrás que implementar el método de copia a mano, y si te olvidas de copiar algún campo — hola, bugs.

7. Herencia: quién puede heredar de quién

  • class: Soporta herencia estándar (jerarquías de clases, métodos virtuales, abstracciones, etc.).
  • struct: No soporta (sólo puede implementar interfaces).
  • record: Soporta herencia, pero con algunas limitaciones (por ejemplo, sólo entre tipos record, no con class).
  • record struct: No soporta, igual que los struct normales.
record Animal(string Name);
record Dog(string Name, string Breed) : Animal(Name); // ¡Ok!

class Vehicle { }
class Car : Vehicle { } // ¡Ok!

// struct no puede heredar de struct

Más sobre herencia en el siguiente nivel :P

8. ¿Dónde se usan estos tipos en la práctica?

  • class: Objetos grandes con mucho comportamiento, ciclo de vida largo, estado mutable, jerarquía (por ejemplo, lógica de negocio, componentes de UI).
  • struct: Objetos pequeños por valor, donde importa la copia rápida, sin GC y con mínimo overhead (por ejemplo, coordenadas, colores, sumas — cosas que se clonan fácil y rápido).
  • record: DTO (Data Transfer Object), objetos parámetro, parámetros de configuración, estados inmutables, resultados de cálculos que es cómodo comparar por contenido.
  • record struct: Tipos por valor donde quieres inmutabilidad y comportamiento de records, pero sin asignaciones extra en el heap.

9. Errores típicos y trampas

A veces la gente piensa que record es simplemente "un reemplazo de class". ¡No es así! Si escribes un objeto que debe cambiar su estado durante su vida — usa class.
Si quieres comparar objetos por referencia (por ejemplo, en el patrón singleton o cuando el ciclo de vida del objeto es crítico) — usa class.
Si haces un objeto por valor que debe comportarse como un número o un punto en una cuadrícula — struct o record struct.
Si trabajas con objetos inmutables, fáciles de comparar, que se pasan mucho entre capas, se guardan en colecciones, se loguean y serializan — tu opción es record.

Recuerda también: si declaras un record-clase con propiedades sólo de lectura, pero olvidas los objetos anidados — los campos anidados pueden seguir cambiando si ellos mismos son mutables.


record Student(string Name, List<int> Grades);

var s1 = new Student("Anton", new List<int>() {5,5,5});
var s2 = s1 with { };

s2.Grades.Add(2); // ¡Ambos objetos "malditos" con un dos! s1.Grades == s2.Grades
2
Tarea
C# SELF, nivel 19, lección 4
Bloqueada
Creación de un record y comparación de objetos
Creación de un record y comparación de objetos
2
Tarea
C# SELF, nivel 19, lección 4
Bloqueada
Uso del operador `with` para escribir datos
Uso del operador `with` para escribir datos
1
Cuestionario/control
Problema con DTO, nivel 19, lección 4
No disponible
Problema con DTO
DTO, record y with
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION