A lo largo de este módulo, todo lo que has modelado —MaterialBibliotecario, Libro, Revista, Socio, Prestamo— son clases: tipos por referencia, pensados para representar entidades con identidad propia y comportamiento (recuerda Prestar(), Devolver(), Describir()). Pero no todos los datos de BiblioTech encajan bien en ese molde: la dirección postal de un socio, o el rango de fechas de un préstamo, son datos pequeños, sin comportamiento propio relevante, donde lo que importa es el valor que contienen, no la identidad del objeto que los representa. Esta última lección del módulo cierra el círculo de la orientación a objetos presentando los structs (tipos por valor) y los records (una forma moderna y concisa de modelar datos inmutables), y te ayudará a decidir cuándo cada uno es más adecuado que una clase tradicional.

Contenido

  1. Recordatorio: tipos por valor frente a tipos por referencia
  2. struct: definir un tipo por valor
  3. Cuándo usar struct: datos pequeños e inmutables
  4. record: igualdad por valor e impresión legible para clases
  5. record struct: lo mejor de ambos mundos
  6. Comparación completa: class, struct, record, record struct
  7. Aplicando la elección correcta en BiblioTech

  1. Recordatorio: tipos por valor frente a tipos por referencia

En la primera lección del módulo viste que las clases son tipos por referencia: copiar una variable de tipo Libro no duplica el objeto, sino que crea una segunda variable que apunta al mismo objeto en memoria.

Libro original = new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
Libro copia = original;
copia.Titulo = "Otro titulo";
Console.WriteLine(original.Titulo); // "Otro titulo" -- el original tambien cambio

Un tipo por valor, en cambio, se comporta de forma opuesta: al copiar una variable, se copian todos sus datos, creando una segunda instancia completamente independiente. Modificar la copia nunca afecta al original. Los tipos primitivos que ya conoces (int, double, bool, char) son tipos por valor; en esta lección aprenderás a crear tus propios tipos por valor con struct.

Tipo por referencia (class) Tipo por valor (struct)
Al copiar una variable Se copia la referencia; ambas variables apuntan al mismo objeto Se copian los datos; son dos instancias independientes
Modificar la copia afecta al original No
Ejemplos ya vistos Libro, Socio, Prestamo int, double, bool, DateTime

  1. struct: definir un tipo por valor

Un struct se define de forma muy parecida a una clase, cambiando la palabra clave class por struct:

struct Direccion
{
    public string Calle;
    public string Ciudad;
    public string CodigoPostal;

    public Direccion(string calle, string ciudad, string codigoPostal)
    {
        Calle = calle;
        Ciudad = ciudad;
        CodigoPostal = codigoPostal;
    }

    public override string ToString()
    {
        return $"{Calle}, {Ciudad} ({CodigoPostal})";
    }
}

Y se usa exactamente igual que una clase en apariencia:

Direccion direccionSocio = new Direccion("Calle Mayor 10", "Madrid", "28013");
Console.WriteLine(direccionSocio); // Calle Mayor 10, Madrid (28013)

Pero su comportamiento al copiarla es el de un tipo por valor:

Direccion direccionOriginal = new Direccion("Calle Mayor 10", "Madrid", "28013");
Direccion direccionCopiada = direccionOriginal; // se copian TODOS los datos, no una referencia

direccionCopiada.Ciudad = "Barcelona";

Console.WriteLine(direccionOriginal.Ciudad); // "Madrid" -- el original NO cambio
Console.WriteLine(direccionCopiada.Ciudad);  // "Barcelona"

A diferencia de lo que ocurría con Libro (una clase) al principio de esta lección, modificar direccionCopiada no afecta en absoluto a direccionOriginal: son dos structs independientes, cada uno con su propia copia de los datos.

Ahora se puede añadir esta dirección al modelo de Socio:

class Socio
{
    public int Id { get; }
    public string Nombre { get; set; }
    public Direccion Direccion { get; set; }

    public Socio(int id, string nombre)
    {
        Id = id;
        Nombre = nombre;
    }
}
Socio socio1 = new Socio(1, "Ana Martinez");
socio1.Direccion = new Direccion("Calle Mayor 10", "Madrid", "28013");

Console.WriteLine(socio1.Direccion); // Calle Mayor 10, Madrid (28013)

  1. Cuándo usar struct: datos pequeños e inmutables

No todo debería ser un struct: son adecuados en un conjunto concreto de situaciones. La regla práctica más citada en la documentación de .NET es preferir struct cuando se cumplen, a la vez, varias condiciones:

  • El tipo representa un único valor conceptual pequeño (pocos campos), como una dirección, una coordenada o un rango de fechas.
  • Sus instancias son, idealmente, inmutables (no cambian tras crearse) o cambian con poca frecuencia.
  • No necesita participar en una jerarquía de herencia (un struct no puede heredar de otro struct ni de una clase, aunque sí puede implementar interfaces, que verás en el Módulo 4).
  • Copiarlo repetidamente no supone un coste de rendimiento relevante (structs muy grandes pueden ser más costosos de copiar que pasar una referencia).

Un segundo ejemplo útil en BiblioTech es un rango de fechas, por ejemplo para representar el periodo previsto de un préstamo:

struct RangoFechas
{
    public DateTime Inicio;
    public DateTime Fin;

    public RangoFechas(DateTime inicio, DateTime fin)
    {
        Inicio = inicio;
        Fin = fin;
    }

    public int DuracionEnDias()
    {
        return (Fin - Inicio).Days;
    }
}
RangoFechas periodoPrestamo = new RangoFechas(new DateTime(2026, 1, 10), new DateTime(2026, 1, 24));
Console.WriteLine($"Duracion del prestamo: {periodoPrestamo.DuracionEnDias()} dias"); // 14 dias
Señal ¿struct o class?
Dato pequeño, pocos campos, sin identidad propia relevante (una dirección, un rango de fechas) struct
Necesita herencia de otras clases, o representa una entidad con identidad propia (Libro, Socio, Prestamo) class
Se va a copiar con mucha frecuencia y es muy pequeño struct (copiar es barato)
Se va a compartir y modificar desde varias partes del programa, esperando que los cambios se vean en todas partes class (el comportamiento por referencia es justo lo que se busca)

  1. record: igualdad por valor e impresión legible para clases

Además de struct, C# moderno ofrece los records, pensados para modelar datos de forma concisa e inmutable, principalmente con dos ventajas automáticas frente a una clase tradicional: igualdad por valor y una implementación de ToString() legible, ambas generadas por el compilador sin que tengas que escribirlas tú.

record ResumenPrestamo(string TituloLibro, string NombreSocio, DateTime FechaPrestamo);

Esta única línea —usando la llamada sintaxis posicional, con los parámetros entre paréntesis junto al nombre del record— ya define tres propiedades de solo lectura (TituloLibro, NombreSocio, FechaPrestamo), un constructor que las recibe todas, un ToString() legible y una comparación de igualdad basada en el valor de sus propiedades, todo generado automáticamente.

ResumenPrestamo resumen1 = new ResumenPrestamo("Rayuela", "Ana Martinez", new DateTime(2026, 1, 10));
ResumenPrestamo resumen2 = new ResumenPrestamo("Rayuela", "Ana Martinez", new DateTime(2026, 1, 10));

Console.WriteLine(resumen1);
// ResumenPrestamo { TituloLibro = Rayuela, NombreSocio = Ana Martinez, FechaPrestamo = 10/01/2026 0:00:00 }

Console.WriteLine(resumen1 == resumen2); // True: misma informacion, aunque son dos objetos distintos en memoria

Compara esto con lo que ocurriría si ResumenPrestamo fuera una clase tradicional: == compararía si ambas variables apuntan al mismo objeto en memoria (igualdad por referencia), y resumen1 == resumen2 daría false, aunque contuvieran exactamente los mismos datos, porque new habría creado dos objetos distintos. Los records redefinen == (y Equals) para comparar, en su lugar, el contenido: dos records son iguales si todas sus propiedades lo son.

ResumenPrestamo es un buen ejemplo de uso real: es un dato de solo lectura, pensado para mostrar o exportar un resumen de un préstamo (por ejemplo, a partir de un objeto Prestamo existente), sin la necesidad de comportamiento propio ni de identidad mutable —justo el perfil que un record modela mejor que una clase tradicional.

ResumenPrestamo CrearResumen(Prestamo prestamo)
{
    return new ResumenPrestamo(prestamo.Libro.Titulo, prestamo.Socio.Nombre, prestamo.FechaPrestamo);
}

Nótese que, por defecto, un record (a diferencia de un struct) sigue siendo un tipo por referencia por debajo: lo que aporta no es el comportamiento de copia por valor, sino la igualdad por valor y el ToString() automático.

  1. record struct: lo mejor de ambos mundos

C# también permite combinar ambas ideas con record struct: un tipo que es, a la vez, un tipo por valor (como struct) y que obtiene automáticamente la igualdad por valor y el ToString() generado (como record). Retomando el ejemplo de Direccion del apartado 2, esta sería su versión como record struct:

record struct Direccion(string Calle, string Ciudad, string CodigoPostal);

Esta única línea sustituye por completo al struct Direccion escrito a mano en el apartado 2: mismo comportamiento de copia por valor, pero con constructor, ToString() e igualdad generados automáticamente, sin escribir una sola línea más.

Direccion direccion1 = new Direccion("Calle Mayor 10", "Madrid", "28013");
Direccion direccion2 = new Direccion("Calle Mayor 10", "Madrid", "28013");

Console.WriteLine(direccion1);              // Direccion { Calle = Calle Mayor 10, Ciudad = Madrid, CodigoPostal = 28013 }
Console.WriteLine(direccion1 == direccion2); // True: igualdad por valor, ademas de comportamiento de struct al copiar

  1. Comparación completa: class, struct, record, record struct

class struct record (class) record struct
Comportamiento al copiar Por referencia Por valor Por referencia Por valor
Igualdad (==) por defecto Por referencia (mismo objeto) Por valor de sus campos Por valor de sus propiedades Por valor de sus propiedades
ToString() legible generado automáticamente No (hay que escribirlo) No (hay que escribirlo)
Soporta herencia de clases No Sí (entre records) No
Uso típico en BiblioTech Entidades con identidad y comportamiento: Libro, Socio, Prestamo Datos pequeños por valor con comportamiento propio: RangoFechas Datos inmutables para mostrar/exportar: ResumenPrestamo Datos pequeños e inmutables, con igualdad y ToString() gratis: Direccion

  1. Aplicando la elección correcta en BiblioTech

Con todo lo visto en el módulo, así queda repartido el modelo de dominio completo de BiblioTech entre las distintas herramientas de C#:

classDiagram
    class MaterialBibliotecario {
        <<abstract>>
        +string Titulo
        +string Autor
        +bool Disponible
        +Prestar()
        +Devolver()
        +Describir()* string
    }
    class Libro {
        +string Isbn
    }
    class Revista {
        +int NumeroEdicion
    }
    class Socio {
        +int Id
        +string Nombre
        +Direccion Direccion
    }
    class Prestamo {
        +Libro Libro
        +Socio Socio
        +DateTime FechaPrestamo
        +DateTime? FechaDevolucion
        +RegistrarDevolucion()
    }
    MaterialBibliotecario <|-- Libro
    MaterialBibliotecario <|-- Revista
    Prestamo --> Libro
    Prestamo --> Socio
    Socio --> Direccion
  • Clases (MaterialBibliotecario, Libro, Revista, Socio, Prestamo): entidades con identidad propia, estado mutable controlado (encapsulamiento) y comportamiento (Prestar(), Describir()...). El núcleo del modelo, construido en las lecciones anteriores.
  • struct (RangoFechas): un valor pequeño y autocontenido, sin identidad propia, usado por valor allí donde haga falta calcular una duración.
  • record struct (Direccion): un dato pequeño e inmutable asociado a un Socio, donde interesa tanto el comportamiento de copia por valor como la igualdad y el ToString() automáticos.
  • record (ResumenPrestamo): una instantánea inmutable de un préstamo, pensada para mostrar o exportar información, sin necesidad de comportamiento ni de identidad mutable.

Errores Comunes y Consejos

  • Usar struct para una entidad con identidad propia: si un tipo necesita comportamiento rico, mutabilidad libre o participar en una jerarquía de herencia (como Libro o Prestamo), debe ser una class, no un struct; forzar un struct ahí generaría copias inesperadas de datos que deberían compartirse.
  • Sorprenderse porque una copia de un struct no refleja los cambios: es precisamente el comportamiento esperado de un tipo por valor; si necesitas que dos variables compartan el mismo dato y ambas vean los cambios, necesitas una class, no un struct.
  • Confundir record con record struct: un record a secas sigue siendo un tipo por referencia (gana igualdad por valor y ToString(), pero no el comportamiento de copia); si además quieres el comportamiento de copia por valor, necesitas record struct.
  • Definir structs muy grandes: si un struct acumula muchos campos, copiarlo deja de ser barato y puede perjudicar el rendimiento; en ese caso, una class (o repensar el diseño) suele ser mejor opción.
  • Consejo: ante la duda entre class y struct/record struct, pregúntate si el tipo representa "un valor" (una fecha, una dirección, una coordenada) o "una entidad" (algo con identidad propia que cambia a lo largo del tiempo, como un socio o un préstamo); la respuesta casi siempre señala la elección correcta.

Ejercicios

  1. Define un struct RangoFechas con las propiedades Inicio y Fin (ambas DateTime), un constructor que las reciba, y un método int DuracionEnDias() que devuelva la diferencia en días entre ambas fechas. Crea dos rangos de fechas distintos y muestra su duración.

  2. Define record struct Direccion(string Calle, string Ciudad, string CodigoPostal); y compáralo con la versión manual del apartado 2: crea dos direcciones con los mismos datos usando el record struct y comprueba con == que se consideran iguales, y muestra una de ellas directamente con Console.WriteLine para comprobar el ToString() automático.

  3. Define record ResumenPrestamo(string TituloLibro, string NombreSocio, DateTime FechaPrestamo);. Crea dos objetos ResumenPrestamo con exactamente los mismos valores pero creados por separado con new, y comprueba con == que, a pesar de ser dos objetos distintos en memoria, se consideran iguales.

Soluciones

struct RangoFechas
{
    public DateTime Inicio;
    public DateTime Fin;

    public RangoFechas(DateTime inicio, DateTime fin)
    {
        Inicio = inicio;
        Fin = fin;
    }

    public int DuracionEnDias()
    {
        return (Fin - Inicio).Days;
    }
}

RangoFechas rango1 = new RangoFechas(new DateTime(2026, 1, 10), new DateTime(2026, 1, 24));
RangoFechas rango2 = new RangoFechas(new DateTime(2026, 2, 1), new DateTime(2026, 2, 5));

Console.WriteLine(rango1.DuracionEnDias()); // 14
Console.WriteLine(rango2.DuracionEnDias()); // 4
record struct Direccion(string Calle, string Ciudad, string CodigoPostal);

Direccion direccion1 = new Direccion("Calle Mayor 10", "Madrid", "28013");
Direccion direccion2 = new Direccion("Calle Mayor 10", "Madrid", "28013");

Console.WriteLine(direccion1 == direccion2); // True
Console.WriteLine(direccion1); // Direccion { Calle = Calle Mayor 10, Ciudad = Madrid, CodigoPostal = 28013 }
record ResumenPrestamo(string TituloLibro, string NombreSocio, DateTime FechaPrestamo);

ResumenPrestamo resumen1 = new ResumenPrestamo("Ficciones", "Luis Gomez", new DateTime(2026, 3, 5));
ResumenPrestamo resumen2 = new ResumenPrestamo("Ficciones", "Luis Gomez", new DateTime(2026, 3, 5));

Console.WriteLine(resumen1 == resumen2); // True

Aunque resumen1 y resumen2 se crean por separado con new (y por tanto ocupan posiciones distintas en memoria), el operador == de un record compara sus propiedades una a una, así que el resultado es True.

Conclusión

Con esta lección se completa el Módulo 3: Programación Orientada a Objetos. Has aprendido a definir clases y crear objetos, a dotarlos de comportamiento con métodos, a garantizar su correcta inicialización con constructores, a compartir código mediante herencia, a tratar distintos tipos de forma uniforme con polimorfismo, a proteger su estado interno con encapsulamiento, a expresar conceptos puramente base con clases abstractas, y, en esta última lección, a elegir entre class, struct, record y record struct según si lo que modelas es una entidad con identidad o un simple valor. El modelo de dominio de BiblioTech queda ahora formado por MaterialBibliotecario (abstracta), Libro y Revista (sus herederas concretas), Socio (con su Direccion), Prestamo, y tipos de apoyo como RangoFechas y ResumenPrestamo.

Este modelo, con sus clases, propiedades y métodos ya definidos, es exactamente el que reutilizarás en el Módulo 4: Conceptos Avanzados de C#: allí aprenderás a definir interfaces para expresar capacidades compartidas sin herencia de clase, a usar delegados y eventos para reaccionar a cambios (como que un libro pase a estar disponible), a aprovechar el pattern matching moderno para simplificar el uso de is/as que ya conoces, a escribir código genérico con genéricos, y sobre todo a almacenar y consultar colecciones enteras de libros, socios y préstamos con colecciones y LINQ —la herramienta que por fin permitirá dejar atrás los arrays de tamaño fijo y trabajar con el catálogo completo de BiblioTech de forma cómoda y expresiva.

Curso de Programación en C#

Módulo 1: Introducción a C#

Módulo 2: Estructuras de Control

Módulo 3: Programación Orientada a Objetos

Módulo 4: Conceptos Avanzados de C#

Módulo 5: Trabajando con Datos

Módulo 6: Temas Avanzados

Módulo 7: Construcción de Aplicaciones

Módulo 8: Mejores Prácticas y Patrones de Diseño

Módulo 9: Proyecto Final

© Copyright 2026. Todos los derechos reservados