Constructores y destructores
Los constructores y destructores son métodos especiales que controlan la creación e inicialización de objetos, así como la limpieza de recursos cuando ya no se necesitan. Estos mecanismos son fundamentales para el ciclo de vida de los objetos en C#, garantizando que se inicialicen correctamente y que los recursos se liberen de manera adecuada.
Dominar estos conceptos te permitirá crear clases más robustas y eficientes, evitando problemas comunes como objetos no inicializados o fugas de memoria. A lo largo de este artículo, exploraremos los diferentes tipos de constructores, cuándo y cómo utilizarlos, y cómo implementar destructores cuando sea necesario.
Constructores
Un constructor es un método especial que se ejecuta automáticamente cuando se crea una instancia de una clase. Su propósito principal es inicializar los campos y propiedades del objeto, preparándolo para su uso.
Características de los constructores
Los constructores tienen características distintivas que los diferencian de los métodos regulares:
Característica | Descripción |
---|---|
Nombre | Debe tener el mismo nombre que la clase |
Valor de retorno | No tienen tipo de retorno (ni siquiera void ) |
Ejecución | Se ejecutan automáticamente al crear el objeto |
Sobrecarga | Pueden tener múltiples versiones con diferentes parámetros |
Constructor predeterminado
Si no defines ningún constructor en tu clase, C# proporciona automáticamente un constructor predeterminado sin parámetros que inicializa los campos con sus valores por defecto:
public class Producto
{
public string Nombre;
public decimal Precio;
// C# proporciona automáticamente:
// public Producto() { }
}
class Program
{
static void Main()
{
// Utiliza el constructor predeterminado
Producto producto = new Producto();
Console.WriteLine($"Nombre: {producto.Nombre}"); // Salida: Nombre:
Console.WriteLine($"Precio: {producto.Precio}"); // Salida: Precio: 0
}
}
Constructor personalizado sin parámetros
Puedes definir tu propio constructor sin parámetros para personalizar la inicialización:
public class Contador
{
private int valor;
// Constructor personalizado sin parámetros
public Contador()
{
valor = 10; // Inicialización personalizada
Console.WriteLine("Contador inicializado con valor 10");
}
public int ObtenerValor()
{
return valor;
}
}
class Program
{
static void Main()
{
Contador contador = new Contador();
// Salida: Contador inicializado con valor 10
Console.WriteLine($"Valor: {contador.ObtenerValor()}");
// Salida: Valor: 10
}
}
Constructores con parámetros
Los constructores con parámetros permiten inicializar objetos con valores específicos proporcionados al momento de la creación:
public class Persona
{
private string nombre;
private int edad;
private string email;
// Constructor con parámetros
public Persona(string nombrePersona, int edadPersona)
{
nombre = nombrePersona;
edad = edadPersona;
email = "no-especificado@ejemplo.com"; // Valor predeterminado
}
public void MostrarInformacion()
{
Console.WriteLine($"Nombre: {nombre}, Edad: {edad}, Email: {email}");
}
}
class Program
{
static void Main()
{
// Crear objetos usando el constructor con parámetros
Persona persona1 = new Persona("Ana García", 25);
Persona persona2 = new Persona("Carlos López", 30);
persona1.MostrarInformacion();
// Salida: Nombre: Ana García, Edad: 25, Email: no-especificado@ejemplo.com
persona2.MostrarInformacion();
// Salida: Nombre: Carlos López, Edad: 30, Email: no-especificado@ejemplo.com
}
}
Sobrecarga de constructores
Puedes definir múltiples constructores con diferentes parámetros para ofrecer distintas formas de inicializar objetos:
public class CuentaBancaria
{
private string titular;
private decimal saldo;
private string numeroCuenta;
// Constructor con todos los parámetros
public CuentaBancaria(string nombreTitular, decimal saldoInicial, string numero)
{
titular = nombreTitular;
saldo = saldoInicial;
numeroCuenta = numero;
}
// Constructor con titular y saldo (genera número automáticamente)
public CuentaBancaria(string nombreTitular, decimal saldoInicial)
{
titular = nombreTitular;
saldo = saldoInicial;
numeroCuenta = GenerarNumeroCuenta();
}
// Constructor solo con titular (saldo inicial de 0)
public CuentaBancaria(string nombreTitular)
{
titular = nombreTitular;
saldo = 0;
numeroCuenta = GenerarNumeroCuenta();
}
private string GenerarNumeroCuenta()
{
Random random = new Random();
return $"CTA-{random.Next(10000, 99999)}";
}
public void MostrarInformacion()
{
Console.WriteLine($"Titular: {titular}");
Console.WriteLine($"Saldo: ${saldo:F2}");
Console.WriteLine($"Número: {numeroCuenta}");
Console.WriteLine();
}
}
class Program
{
static void Main()
{
// Usando diferentes constructores
CuentaBancaria cuenta1 = new CuentaBancaria("María Fernández", 1000.00m, "CTA-12345");
CuentaBancaria cuenta2 = new CuentaBancaria("José Martínez", 500.00m);
CuentaBancaria cuenta3 = new CuentaBancaria("Laura Sánchez");
cuenta1.MostrarInformacion();
cuenta2.MostrarInformacion();
cuenta3.MostrarInformacion();
}
}
Encadenamiento de constructores con this
Puedes hacer que un constructor llame a otro constructor de la misma clase usando la palabra clave this
, evitando duplicación de código:
public class Rectangulo
{
private double ancho;
private double alto;
private string color;
// Constructor principal con todos los parámetros
public Rectangulo(double anchoRect, double altoRect, string colorRect)
{
ancho = anchoRect;
alto = altoRect;
color = colorRect;
Console.WriteLine($"Rectángulo creado: {ancho}x{alto}, color {color}");
}
// Constructor que asume color blanco
public Rectangulo(double anchoRect, double altoRect) : this(anchoRect, altoRect, "blanco")
{
Console.WriteLine("Color establecido automáticamente a blanco");
}
// Constructor para cuadrado (mismo ancho y alto)
public Rectangulo(double lado) : this(lado, lado, "blanco")
{
Console.WriteLine("Cuadrado creado");
}
public double CalcularArea()
{
return ancho * alto;
}
}
class Program
{
static void Main()
{
Console.WriteLine("=== Rectángulo completo ===");
Rectangulo rect1 = new Rectangulo(5.0, 3.0, "azul");
Console.WriteLine("\n=== Rectángulo sin color ===");
Rectangulo rect2 = new Rectangulo(4.0, 2.0);
Console.WriteLine("\n=== Cuadrado ===");
Rectangulo cuadrado = new Rectangulo(3.0);
Console.WriteLine($"\nÁreas calculadas:");
Console.WriteLine($"Rectángulo 1: {rect1.CalcularArea()}");
Console.WriteLine($"Rectángulo 2: {rect2.CalcularArea()}");
Console.WriteLine($"Cuadrado: {cuadrado.CalcularArea()}");
}
}
Destructores
Un destructor es un método especial que se ejecuta cuando un objeto está siendo eliminado de la memoria por el recolector de basura (Garbage Collector). Se utiliza principalmente para liberar recursos no administrados como archivos, conexiones de red o memoria no administrada.
Características de los destructores
Los destructores tienen características específicas que los distinguen:
Característica | Descripción |
---|---|
Sintaxis | Se define con ~ seguido del nombre de la clase |
Parámetros | No pueden tener parámetros |
Modificadores | No pueden tener modificadores de acceso |
Herencia | Se llaman automáticamente en orden inverso a la herencia |
Ejecución | El momento exacto de ejecución no es determinista |
Ejemplo básico de destructor
public class RecursoSimulado
{
private string nombre;
private bool recursoAbierto;
public RecursoSimulado(string nombreRecurso)
{
nombre = nombreRecurso;
recursoAbierto = true;
Console.WriteLine($"Recurso '{nombre}' adquirido");
}
// Destructor
~RecursoSimulado()
{
if (recursoAbierto)
{
Console.WriteLine($"Destructor: Liberando recurso '{nombre}'");
LiberarRecurso();
}
}
public void LiberarRecurso()
{
if (recursoAbierto)
{
Console.WriteLine($"Recurso '{nombre}' liberado manualmente");
recursoAbierto = false;
}
}
}
class Program
{
static void Main()
{
Console.WriteLine("=== Creando recursos ===");
CrearYUsarRecursos();
Console.WriteLine("\n=== Forzando recolección de basura ===");
GC.Collect(); // Fuerza la recolección de basura (solo para demostración)
GC.WaitForPendingFinalizers();
Console.WriteLine("Programa terminado");
}
static void CrearYUsarRecursos()
{
RecursoSimulado recurso1 = new RecursoSimulado("Archivo1.txt");
RecursoSimulado recurso2 = new RecursoSimulado("Conexión-DB");
// recurso2 se libera manualmente
recurso2.LiberarRecurso();
// recurso1 se libera automáticamente por el destructor
}
}
Patrón Dispose y IDisposable
Para un mejor control sobre la liberación de recursos, es recomendable implementar la interfaz IDisposable
junto con el patrón Dispose:
using System;
public class GestorArchivos : IDisposable
{
private string rutaArchivo;
private bool disposed = false;
public GestorArchivos(string ruta)
{
rutaArchivo = ruta;
Console.WriteLine($"Archivo '{rutaArchivo}' abierto");
}
// Implementación de IDisposable
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this); // Evita que se ejecute el destructor
}
// Método protegido para la liberación de recursos
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
// Liberar recursos administrados
Console.WriteLine($"Liberando recursos administrados para '{rutaArchivo}'");
}
// Liberar recursos no administrados
Console.WriteLine($"Liberando recursos no administrados para '{rutaArchivo}'");
disposed = true;
}
}
// Destructor como respaldo
~GestorArchivos()
{
Console.WriteLine($"Destructor ejecutado para '{rutaArchivo}'");
Dispose(false);
}
public void ProcesarArchivo()
{
if (disposed)
throw new ObjectDisposedException(nameof(GestorArchivos));
Console.WriteLine($"Procesando archivo '{rutaArchivo}'");
}
}
class Program
{
static void Main()
{
Console.WriteLine("=== Uso con using (recomendado) ===");
using (GestorArchivos gestor1 = new GestorArchivos("documento1.txt"))
{
gestor1.ProcesarArchivo();
} // Dispose se llama automáticamente aquí
Console.WriteLine("\n=== Uso manual ===");
GestorArchivos gestor2 = new GestorArchivos("documento2.txt");
try
{
gestor2.ProcesarArchivo();
}
finally
{
gestor2.Dispose(); // Liberación manual
}
Console.WriteLine("\n=== Sin liberación manual (usa destructor) ===");
GestorArchivos gestor3 = new GestorArchivos("documento3.txt");
gestor3.ProcesarArchivo();
// gestor3 se libera por el destructor cuando el GC lo ejecute
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
Constructores estáticos
Los constructores estáticos se ejecutan una sola vez antes de crear la primera instancia de la clase o acceder a cualquier miembro estático:
public class ConfiguracionApp
{
public static string RutaConfiguracion { get; private set; }
public static DateTime FechaInicializacion { get; private set; }
private static int contadorInstancias = 0;
// Constructor estático
static ConfiguracionApp()
{
RutaConfiguracion = @"C:\Config\app.config";
FechaInicializacion = DateTime.Now;
Console.WriteLine("Constructor estático ejecutado");
Console.WriteLine($"Configuración cargada desde: {RutaConfiguracion}");
}
// Constructor de instancia
public ConfiguracionApp()
{
contadorInstancias++;
Console.WriteLine($"Instancia #{contadorInstancias} creada");
}
public static void MostrarEstadisticas()
{
Console.WriteLine($"Instancias creadas: {contadorInstancias}");
Console.WriteLine($"Inicializado el: {FechaInicializacion}");
}
}
class Program
{
static void Main()
{
Console.WriteLine("Programa iniciado");
// El acceso a un miembro estático ejecuta el constructor estático
Console.WriteLine($"Ruta: {ConfiguracionApp.RutaConfiguracion}");
Console.WriteLine("\nCreando instancias...");
ConfiguracionApp config1 = new ConfiguracionApp();
ConfiguracionApp config2 = new ConfiguracionApp();
ConfiguracionApp.MostrarEstadisticas();
}
}
Resumen
Los constructores y destructores son elementos fundamentales para el manejo del ciclo de vida de los objetos en C#. Los constructores garantizan que los objetos se inicialicen correctamente, mientras que los destructores proporcionan un mecanismo para la limpieza de recursos cuando los objetos ya no se necesitan.
Es importante recordar que puedes sobrecargar constructores para ofrecer diferentes formas de inicialización, usar el encadenamiento con this
para evitar duplicación de código, y implementar el patrón IDisposable para un control más preciso sobre la liberación de recursos. Los constructores estáticos te permiten inicializar datos compartidos por todas las instancias de una clase, ejecutándose una sola vez durante el tiempo de vida de la aplicación.