En la lección anterior, cada vez que creabas un objeto Libro tenías que acordarte de asignar
sus cuatro propiedades a mano (Titulo, Autor, Isbn, Disponible) o usar un
inicializador de objeto; si olvidabas Disponible, el libro nacía con false (el valor por
defecto de bool) en lugar de true, sin que nada te avisara del error. Esta lección resuelve
ese problema con los constructores: un método especial que se ejecuta automáticamente en el
instante en que se crea un objeto con new, garantizando que nazca siempre en un estado
coherente. De paso, conocerás su contrapartida —los destructores— y ampliarás el modelo de
dominio de BiblioTech con dos piezas nuevas: Socio y Prestamo, que junto con Libro
formarán el núcleo sobre el que se construirá el resto del curso.
Contenido
- Qué es un constructor y cuándo se ejecuta
- El constructor por defecto
- Constructores con parámetros
- Sobrecarga de constructores
- Encadenar constructores con
this(...) - Inicializadores de propiedades
- Creando la clase
Socio - Uniendo piezas: la clase
Prestamo - Destructores y una mención a
IDisposable
- Qué es un constructor y cuándo se ejecuta
Un constructor es un método especial de una clase que se ejecuta automáticamente cada vez
que se crea un objeto con new. Se distingue de un método normal en dos aspectos: tiene
exactamente el mismo nombre que la clase, y no declara ningún tipo de retorno (ni
siquiera void).
class Libro
{
public string Titulo { get; set; }
public string Autor { get; set; }
public string Isbn { get; set; }
public bool Disponible { get; set; }
public Libro()
{
Disponible = true;
Console.WriteLine("Se ha creado un nuevo libro.");
}
}Libro libro1 = new Libro(); // ejecuta el constructor: imprime el mensaje y pone Disponible = true
Console.WriteLine(libro1.Disponible); // TrueEn cuanto se ejecuta new Libro(), C# reserva memoria para el nuevo objeto y, a continuación,
ejecuta automáticamente el código del constructor —en este caso, asigna Disponible = true y
muestra un mensaje— antes de devolver la referencia al objeto ya creado. Nunca se llama a un
constructor "a mano" como si fuera un método cualquiera (libro1.Libro() sería un error de
compilación); solo se ejecuta como parte de new.
- El constructor por defecto
Si una clase no define ningún constructor propio, C# le proporciona automáticamente uno
implícito, sin parámetros, que no hace nada más que crear el objeto con todas sus propiedades
en su valor por defecto (como viste en la lección anterior). Es lo que ha estado ocurriendo,
sin que lo supieras, en todos los new Libro() de las lecciones previas a esta.
En cuanto defines cualquier constructor propio (como el del apartado anterior), ese
constructor implícito y gratuito desaparece: si sigues necesitando poder crear un Libro
sin argumentos, tendrás que escribir tú mismo ese constructor sin parámetros, de forma
explícita.
- Constructores con parámetros
Lo habitual es que un constructor reciba parámetros para inicializar el objeto con datos concretos desde el primer momento, en lugar de crear un objeto "vacío" y rellenarlo después:
class Libro
{
public string Titulo { get; set; }
public string Autor { get; set; }
public string Isbn { get; set; }
public bool Disponible { get; set; }
public Libro(string titulo, string autor, string isbn)
{
Titulo = titulo;
Autor = autor;
Isbn = isbn;
Disponible = true;
}
}Libro libro1 = new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
Console.WriteLine(libro1.Titulo); // Rayuela
Console.WriteLine(libro1.Disponible); // True (se fija siempre en el constructor)Este constructor garantiza dos cosas a la vez: que no se pueda crear un Libro sin indicar su
título, autor e ISBN (son parámetros obligatorios), y que Disponible siempre empiece en
true sin que quien crea el libro tenga que acordarse de asignarlo. Fíjate en que, dentro del
constructor, los nombres de los parámetros (titulo, en minúscula) son distintos de los
nombres de las propiedades (Titulo, en mayúscula) precisamente para poder distinguirlos con
claridad al escribir Titulo = titulo;.
- Sobrecarga de constructores
Igual que los métodos, los constructores se pueden sobrecargar: una misma clase puede tener varios constructores, siempre que se distingan por su lista de parámetros.
class Libro
{
public string Titulo { get; set; }
public string Autor { get; set; }
public string Isbn { get; set; }
public bool Disponible { get; set; }
public Libro()
{
Titulo = "Titulo desconocido";
Autor = "Autor desconocido";
Isbn = "";
Disponible = true;
}
public Libro(string titulo, string autor, string isbn)
{
Titulo = titulo;
Autor = autor;
Isbn = isbn;
Disponible = true;
}
}Libro libroSinDatos = new Libro(); // usa el constructor sin parametros
Libro libroCompleto = new Libro("Ficciones", "Jorge Luis Borges", "978-84-376-0496-1"); // usa el constructor con 3 parametrosC# elige automáticamente qué constructor ejecutar según el número y tipo de argumentos que le
pases en el new, exactamente igual que con la sobrecarga de métodos vista en la lección
anterior.
- Encadenar constructores con
this(...)
this(...)El ejemplo anterior repite Disponible = true; en los dos constructores: si algún día cambiara
esa lógica de inicialización, habría que recordar actualizarla en los dos sitios. Para evitar
esta duplicación, un constructor puede llamar a otro constructor de la misma clase usando
this(...), delegando en él la inicialización común:
class Libro
{
public string Titulo { get; set; }
public string Autor { get; set; }
public string Isbn { get; set; }
public bool Disponible { get; set; }
public Libro(string titulo, string autor, string isbn)
{
Titulo = titulo;
Autor = autor;
Isbn = isbn;
Disponible = true;
}
public Libro() : this("Titulo desconocido", "Autor desconocido", "")
{
// El cuerpo puede quedar vacio: this(...) ya ha hecho el trabajo
}
}La sintaxis : this(...) se coloca entre la firma del constructor y su cuerpo ({ }), e
indica "antes de ejecutar este constructor, ejecuta primero el otro constructor con estos
argumentos". Así, new Libro() termina ejecutando también la lógica del constructor de tres
parámetros, con valores por defecto para cada uno, sin duplicar la asignación de Disponible.
Esta técnica se conoce como encadenamiento de constructores, y es la forma recomendada de
evitar repetir lógica de inicialización cuando una clase tiene varios constructores.
- Inicializadores de propiedades
Para propiedades que siempre deben arrancar con el mismo valor fijo, independientemente del constructor que se use, C# ofrece una alternativa aún más directa: asignar el valor por defecto en la propia declaración de la propiedad, sin necesidad de tocar ningún constructor:
class Libro
{
public string Titulo { get; set; }
public string Autor { get; set; }
public string Isbn { get; set; }
public bool Disponible { get; set; } = true; // inicializador de propiedad
public Libro(string titulo, string autor, string isbn)
{
Titulo = titulo;
Autor = autor;
Isbn = isbn;
// Ya no hace falta asignar Disponible aqui: siempre empieza en true
}
}Un inicializador de propiedad se ejecuta antes que el cuerpo de cualquier constructor, así que si el constructor asigna después un valor distinto a esa misma propiedad, el valor del constructor prevalece: el inicializador solo actúa como valor de partida. Es la forma más simple y legible de fijar un valor por defecto cuando ese valor no depende de ningún parámetro del constructor.
- Creando la clase
Socio
SocioCon los constructores ya dominados, es el momento de añadir la segunda pieza del modelo de
BiblioTech: la clase Socio, que representa a una persona registrada en la biblioteca con
derecho a pedir libros prestados.
class Socio
{
public int Id { get; set; }
public string Nombre { get; set; }
public Socio(int id, string nombre)
{
Id = id;
Nombre = nombre;
}
public void MostrarFicha()
{
Console.WriteLine($"Socio #{Id}: {Nombre}");
}
}Socio socio1 = new Socio(1, "Ana Martinez");
Socio socio2 = new Socio(2, "Luis Gomez");
socio1.MostrarFicha(); // Socio #1: Ana Martinez
socio2.MostrarFicha(); // Socio #2: Luis GomezSocio sigue exactamente el mismo patrón que Libro: propiedades autoimplementadas (Id,
Nombre), un constructor que exige los datos esenciales al crearlo, y un método
(MostrarFicha()) que aprovecha esos datos. Id identificará a cada socio de forma única
dentro de BiblioTech —una idea que cobrará más importancia en el Módulo 5, al persistir datos
en una base de datos.
- Uniendo piezas: la clase
Prestamo
PrestamoCon Libro y Socio ya definidos, BiblioTech necesita representar el concepto que los
relaciona: un préstamo, que vincula un libro concreto con el socio que lo ha tomado
prestado, junto con las fechas relevantes. Esta es la tercera y última pieza del modelo de
dominio del curso:
class Prestamo
{
public Libro Libro { get; set; }
public Socio Socio { get; set; }
public DateTime FechaPrestamo { get; set; }
public DateTime? FechaDevolucion { get; set; } // null mientras el libro no se ha devuelto
public Prestamo(Libro libro, Socio socio)
{
Libro = libro;
Socio = socio;
FechaPrestamo = DateTime.Now;
FechaDevolucion = null;
}
public void MostrarFicha()
{
string estado = FechaDevolucion is null ? "en curso" : $"devuelto el {FechaDevolucion:d}";
Console.WriteLine($"Prestamo de '{Libro.Titulo}' a {Socio.Nombre} el {FechaPrestamo:d} ({estado})");
}
}Fíjate en tres detalles importantes de esta clase:
- Sus propiedades
LibroySociono son tipos primitivos, sino objetos de otras clases que ya has definido: unPrestamono duplica el título del libro ni el nombre del socio, sino que guarda una referencia a los objetosLibroySociooriginales (recuerda que las clases son tipos por referencia). Si más adelante ese libro se marca como no disponible, elPrestamoque lo referencia "ve" automáticamente el cambio, porque apunta al mismo objeto. FechaDevoluciones de tipoDateTime?(con?): el signo de interrogación indica un tipo que admitenull(nullable), necesario aquí porque, mientras el préstamo sigue en curso, todavía no existe ninguna fecha de devolución real.- El constructor fija
FechaPrestamoal momento actual (DateTime.Now) automáticamente: quien crea unPrestamono tiene que calcular ni pasar la fecha a mano.
Así se usan las tres clases juntas, tal y como se comportarán durante el resto del curso:
Libro libro1 = new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
Socio socio1 = new Socio(1, "Ana Martinez");
libro1.Prestar(); // el libro pasa a Disponible = false (metodo visto en la leccion anterior)
Prestamo prestamo1 = new Prestamo(libro1, socio1);
prestamo1.MostrarFicha(); // Prestamo de 'Rayuela' a Ana Martinez el <fecha de hoy> (en curso)Con esto, el modelo de dominio de BiblioTech queda formado por tres clases que colaboran entre
sí: Libro, Socio y Prestamo. Este trío se mantendrá estable durante el resto del curso, y
lo irás enriqueciendo (herencia, encapsulamiento, colecciones, persistencia...) sin cambiar su
propósito esencial.
- Destructores y una mención a
IDisposable
IDisposableAsí como un constructor se ejecuta al crear un objeto, un destructor (también llamado
finalizador) es un método especial que el recolector de basura (garbage collector) de .NET
ejecuta, en algún momento no determinado, antes de liberar definitivamente la memoria de un
objeto que ya no se usa. Se escribe con el símbolo ~ seguido del nombre de la clase, sin
modificador de acceso ni parámetros:
class Libro
{
// ... propiedades y constructores ...
~Libro()
{
Console.WriteLine($"El objeto Libro '{Titulo}' esta siendo destruido.");
}
}En la práctica, los destructores se usan muy poco en C# moderno, por varias razones: no
puedes controlar cuándo se ejecutan exactamente (depende del recolector de basura, no de
tu código), añaden una sobrecarga de rendimiento, y la mayoría de la limpieza de recursos que
un destructor podría hacer (cerrar ficheros, conexiones de red o de base de datos) se gestiona
hoy en día con la interfaz IDisposable y la instrucción using, que ofrecen un control
determinista y explícito sobre cuándo se libera cada recurso. IDisposable se estudiará junto
con el manejo de ficheros y bases de datos en el Módulo 5; de momento basta con que sepas que
existe, y que es la alternativa preferida frente a los destructores para liberar recursos de
forma fiable.
| Constructor | Destructor | |
|---|---|---|
| Símbolo | Ninguno (mismo nombre que la clase) | ~ antes del nombre de la clase |
| Cuándo se ejecuta | Al crear el objeto con new (inmediato y predecible) |
En algún momento indeterminado, decidido por el recolector de basura |
| Frecuencia de uso en C# moderno | Muy habitual | Rara; se prefiere IDisposable (Módulo 5) |
Errores Comunes y Consejos
- Definir un constructor con parámetros y perder el constructor sin parámetros sin darse
cuenta: en cuanto añades cualquier constructor propio, el constructor por defecto
desaparece; si sigues necesitando
new Libro()sin argumentos, debes declararlo tú mismo. - Repetir lógica de inicialización en varios constructores: si dos o más constructores
comparten parte de su lógica, usa
this(...)para encadenarlos en lugar de copiar y pegar el mismo código en cada uno. - Confundir el nombre del parámetro con el de la propiedad:
Titulo = titulo;asigna a la propiedad (Titulo, con mayúscula) el valor del parámetro (titulo, con minúscula); si escribierastitulo = titulo;por error, no ocurriría nada útil. - Usar
DateTimeen lugar deDateTime?cuando el valor puede no existir todavía: comoFechaDevolucionpuede no tener valor mientras el préstamo está en curso, debe ser un tipo nullable (DateTime?); usarDateTimea secas obligaría a inventar una fecha "falsa" para representar "todavía no devuelto". - Confiar en el destructor para liberar recursos importantes: como no se puede predecir
cuándo se ejecutará, no es una herramienta fiable para cerrar ficheros o conexiones a
tiempo; para eso está
IDisposable(Módulo 5).
Ejercicios
-
Define una clase
Librocon las propiedadesTitulo,Autor,Isbn(todasstring) yDisponible(bool, inicializada mediante un inicializador de propiedad atrue). Añade un único constructorLibro(string titulo, string autor, string isbn)que asigne las tres propiedades de texto. Crea un objeto y comprueba queDisponiblevaletruesin haberlo asignado en el constructor. -
Añade a la clase anterior un segundo constructor sin parámetros que use
this(...)para encadenar con el constructor de tres parámetros, pasando los valores"Titulo desconocido","Autor desconocido"y"". Crea un objeto connew Libro()y muestra suTitulopara comprobar que el encadenamiento funciona. -
Crea la clase
Sociocon las propiedadesId(int) yNombre(string) y un constructorSocio(int id, string nombre). Después, crea un objetoLibro(usando el constructor del ejercicio 1) y un objetoSocio, y usa ambos para construir manualmente un objetoPrestamo(según la clase definida en el apartado 8 de esta lección), mostrando su ficha conMostrarFicha().
Soluciones
class Libro
{
public string Titulo { get; set; }
public string Autor { get; set; }
public string Isbn { get; set; }
public bool Disponible { get; set; } = true;
public Libro(string titulo, string autor, string isbn)
{
Titulo = titulo;
Autor = autor;
Isbn = isbn;
}
}
Libro libro1 = new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
Console.WriteLine(libro1.Disponible); // True
public Libro() : this("Titulo desconocido", "Autor desconocido", "")
{
}
Libro libroVacio = new Libro();
Console.WriteLine(libroVacio.Titulo); // Titulo desconocido
El constructor sin parámetros delega en el de tres parámetros mediante this(...), por lo
que Titulo, Autor e Isbn acaban recibiendo los valores por defecto indicados, sin
duplicar código.
class Socio
{
public int Id { get; set; }
public string Nombre { get; set; }
public Socio(int id, string nombre)
{
Id = id;
Nombre = nombre;
}
}
Libro libro1 = new Libro("Ficciones", "Jorge Luis Borges", "978-84-376-0496-1");
Socio socio1 = new Socio(1, "Ana Martinez");
Prestamo prestamo1 = new Prestamo(libro1, socio1);
prestamo1.MostrarFicha(); // Prestamo de 'Ficciones' a Ana Martinez el <fecha de hoy> (en curso)
Conclusión
En esta lección has aprendido a garantizar que los objetos nazcan siempre en un estado
coherente mediante constructores: el constructor por defecto, constructores con
parámetros, su sobrecarga, el encadenamiento con this(...) y los inicializadores de
propiedad; también has visto qué son los destructores y por qué en C# moderno se prefiere
IDisposable para liberar recursos. Con esto, el modelo de dominio de BiblioTech queda
formado por tres clases que colaboran entre sí: Libro (título, autor, ISBN, disponibilidad),
Socio (identificador y nombre) y Prestamo (qué libro, qué socio, cuándo se prestó y cuándo
se devolvió).
Este trío de clases, sin embargo, tiene ahora mismo cierta duplicación oculta: si BiblioTech
quisiera prestar también revistas, tendría que repetir en una nueva clase Revista las mismas
propiedades (Titulo, Autor, Disponible) y métodos (Prestar(), Devolver()) que ya
tiene Libro. En la próxima lección, Herencia, aprenderás a evitar esa duplicación
extrayendo lo común a una clase base compartida, MaterialBibliotecario, de la que Libro (y
la nueva Revista) heredarán.
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
