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
- Recordatorio: tipos por valor frente a tipos por referencia
struct: definir un tipo por valor- Cuándo usar
struct: datos pequeños e inmutables record: igualdad por valor e impresión legible para clasesrecord struct: lo mejor de ambos mundos- Comparación completa:
class,struct,record,record struct - Aplicando la elección correcta en BiblioTech
- 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 cambioUn 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 | Sí | No |
| Ejemplos ya vistos | Libro, Socio, Prestamo |
int, double, bool, DateTime |
struct: definir un tipo por valor
struct: definir un tipo por valorUn 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)
- Cuándo usar
struct: datos pequeños e inmutables
struct: datos pequeños e inmutablesNo 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
structno puede heredar de otrostructni 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) |
record: igualdad por valor e impresión legible para clases
record: igualdad por valor e impresión legible para clasesAdemá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ú.
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 memoriaCompara 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.
record struct: lo mejor de ambos mundos
record struct: lo mejor de ambos mundosC# 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:
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
- Comparación completa:
class, struct, record, record struct
class, struct, record, record structclass |
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) | Sí | Sí |
| Soporta herencia de clases | Sí | 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 |
- 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 unSocio, donde interesa tanto el comportamiento de copia por valor como la igualdad y elToString()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
structpara una entidad con identidad propia: si un tipo necesita comportamiento rico, mutabilidad libre o participar en una jerarquía de herencia (comoLibrooPrestamo), debe ser unaclass, no unstruct; forzar unstructahí generaría copias inesperadas de datos que deberían compartirse. - Sorprenderse porque una copia de un
structno 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 unaclass, no unstruct. - Confundir
recordconrecord struct: unrecorda secas sigue siendo un tipo por referencia (gana igualdad por valor yToString(), pero no el comportamiento de copia); si además quieres el comportamiento de copia por valor, necesitasrecord struct. - Definir structs muy grandes: si un
structacumula muchos campos, copiarlo deja de ser barato y puede perjudicar el rendimiento; en ese caso, unaclass(o repensar el diseño) suele ser mejor opción. - Consejo: ante la duda entre
classystruct/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
-
Define un
struct RangoFechascon las propiedadesInicioyFin(ambasDateTime), un constructor que las reciba, y un métodoint DuracionEnDias()que devuelva la diferencia en días entre ambas fechas. Crea dos rangos de fechas distintos y muestra su duración. -
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 elrecord structy comprueba con==que se consideran iguales, y muestra una de ellas directamente conConsole.WriteLinepara comprobar elToString()automático. -
Define
record ResumenPrestamo(string TituloLibro, string NombreSocio, DateTime FechaPrestamo);. Crea dos objetosResumenPrestamocon exactamente los mismos valores pero creados por separado connew, 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#
- Introducción a C#
- Configuración del Entorno de Desarrollo
- Programa Hola Mundo
- Sintaxis y Estructura Básica
- Variables y Tipos de Datos
- Arrays y Cadenas de Texto
Módulo 2: Estructuras de Control
Módulo 3: Programación Orientada a Objetos
- Clases y Objetos
- Métodos
- Constructores y Destructores
- Herencia
- Polimorfismo
- Encapsulamiento
- Abstracción
- Structs y Records: Tipos por Valor y por Referencia
Módulo 4: Conceptos Avanzados de C#
- Interfaces
- Delegados y Eventos
- Pattern Matching y Características Modernas de C#
- Genéricos
- Colecciones
- LINQ (Consulta Integrada en el Lenguaje)
- Programación Asíncrona
Módulo 5: Trabajando con Datos
- Entrada/Salida de Archivos
- Serialización
- Conectividad con Bases de Datos
- Entity Framework
- Trabajo con JSON y Consumo de APIs REST
Módulo 6: Temas Avanzados
- Reflexión
- Atributos
- Programación Dinámica
- Gestión de Memoria y Recolección de Basura
- Multihilo y Programación Paralela
Módulo 7: Construcción de Aplicaciones
- Formularios de Windows
- WPF (Windows Presentation Foundation)
- ASP.NET Core
- Blazor
- Xamarin y .NET MAUI
Módulo 8: Mejores Prácticas y Patrones de Diseño
- Estándares de Codificación y Mejores Prácticas
- Patrones de Diseño
- Inyección de Dependencias e Inversión de Control
- Pruebas Unitarias
- Revisión y Refactorización de Código
