En esta página
Herencia e interfaces en C#
Herencia en C#
La herencia permite crear nuevas clases basadas en clases existentes, reutilizando y extendiendo su funcionalidad. En C#, una clase puede heredar de una sola clase (herencia simple), pero puede implementar múltiples interfaces.
// Sintaxis de herencia: clase Hija : ClasePadre
public class Animal
{
public string Nombre { get; init; } = string.Empty;
public string Sonido() => "...";
public virtual string Describir() => $"{Nombre} hace '{Sonido()}'";
}
public class Perro : Animal
{
public new string Sonido() => "Guau"; // oculta (hide), no sobreescribe
}
public class Gato : Animal
{
public override string Describir() // sobreescribe el virtual
=> base.Describir() + " [es un gato]";
}virtual, override y abstract
| Palabra clave | Significado |
|---|---|
virtual |
El método puede sobreescribirse en subclases |
override |
Sobreescribe un método virtual o abstracto de la clase base |
abstract |
El método debe implementarse en subclases (clase debe ser abstracta) |
sealed |
Impide que el método sea sobreescrito en subclases |
new |
Oculta el miembro de la clase base (diferente a override) |
public abstract class Figura
{
// Abstract: SIN implementación, DEBE implementarse
public abstract double Area();
public abstract double Perimetro();
// Virtual: CON implementación, PUEDE sobreescribirse
public virtual void Dibujar()
=> Console.WriteLine($"Dibujando {GetType().Name}...");
// Concreto: no puede sobreescribirse a menos que sea virtual
public string Tipo() => GetType().Name;
}
public class Circulo : Figura
{
public double Radio { get; init; }
public override double Area() => Math.PI * Radio * Radio;
public override double Perimetro() => 2 * Math.PI * Radio;
// Sobreescribe y luego sella — nadie puede sobreescribir en subclases
public sealed override void Dibujar()
=> Console.WriteLine($"○ Círculo de radio {Radio:F2}");
}
public class Rectangulo : Figura
{
public double Ancho { get; init; }
public double Alto { get; init; }
public override double Area() => Ancho * Alto;
public override double Perimetro() => 2 * (Ancho + Alto);
}Polimorfismo
El polimorfismo permite tratar objetos de distintos tipos de forma uniforme a través de su tipo base o interfaz:
List<Figura> figuras = new()
{
new Circulo { Radio = 5 },
new Rectangulo { Ancho = 4, Alto = 6 },
new Circulo { Radio = 3 },
};
double areaTotal = 0;
foreach (Figura f in figuras)
{
f.Dibujar(); // método sobreescrito específico
areaTotal += f.Area(); // polimorfismo en acción
Console.WriteLine($" Área: {f.Area():F2}");
}
Console.WriteLine($"Área total: {areaTotal:F2}");Interfaces
Una interfaz define un contrato sin implementación (salvo los métodos por defecto de C# 8+). Cualquier tipo que implemente la interfaz garantiza que tiene esos miembros:
public interface IRepositorio<T> where T : class
{
Task<T?> ObtenerPorIdAsync(int id);
Task<List<T>> ListarAsync();
Task<T> CrearAsync(T entidad);
Task<T> ActualizarAsync(T entidad);
Task<bool> EliminarAsync(int id);
}
public interface IValidable
{
bool EsValido();
IEnumerable<string> ObtenerErrores();
}Implementación de múltiples interfaces
public class Empleado : IRepositorio<Empleado>, IValidable, IComparable<Empleado>
{
public int Id { get; init; }
public string Nombre { get; set; } = string.Empty;
public decimal Salario { get; set; }
// IValidable
public bool EsValido()
=> !string.IsNullOrEmpty(Nombre) && Salario > 0;
public IEnumerable<string> ObtenerErrores()
{
if (string.IsNullOrEmpty(Nombre)) yield return "Nombre es requerido";
if (Salario <= 0) yield return "Salario debe ser positivo";
}
// IComparable<Empleado>
public int CompareTo(Empleado? other)
=> other is null ? 1 : Nombre.CompareTo(other.Nombre);
// IRepositorio<Empleado> — implementación simplificada
public Task<Empleado?> ObtenerPorIdAsync(int id) => Task.FromResult<Empleado?>(null);
public Task<List<Empleado>> ListarAsync() => Task.FromResult(new List<Empleado>());
public Task<Empleado> CrearAsync(Empleado e) => Task.FromResult(e);
public Task<Empleado> ActualizarAsync(Empleado e) => Task.FromResult(e);
public Task<bool> EliminarAsync(int id) => Task.FromResult(true);
}Métodos de interfaz por defecto (C# 8+)
public interface ILogger
{
void Log(string mensaje, string nivel);
// Método con implementación por defecto
void LogInfo(string mensaje) => Log(mensaje, "INFO");
void LogWarning(string mensaje) => Log(mensaje, "WARNING");
void LogError(string mensaje) => Log(mensaje, "ERROR");
}
public class ConsoleLogger : ILogger
{
// Solo necesita implementar el método abstracto
public void Log(string mensaje, string nivel)
=> Console.WriteLine($"[{nivel}] {DateTime.Now:HH:mm:ss} — {mensaje}");
}
ILogger logger = new ConsoleLogger();
logger.LogInfo("Aplicación iniciada"); // usa el default
logger.LogError("Error en el sistema"); // usa el default
logger.Log("Mensaje custom", "DEBUG"); // implementación directaPrincipio de sustitución de Liskov
Una instancia de una subclase debe poder sustituir a su clase base sin romper el programa:
// MAL: viola Liskov — el cuadrado no es un rectángulo correcto
public class RectanguloMal
{
public virtual int Ancho { get; set; }
public virtual int Alto { get; set; }
public int Area() => Ancho * Alto;
}
public class CuadradoMal : RectanguloMal
{
public override int Ancho { set => base.Ancho = base.Alto = value; }
public override int Alto { set => base.Ancho = base.Alto = value; }
}
// BIEN: modela la realidad con interfaces
public interface IForma { double Area(); }
public class RectanguloBien : IForma { public double Area() => Ancho * Alto; public double Ancho; public double Alto; }
public class CuadradoBien : IForma { public double Area() => Lado * Lado; public double Lado; }Práctica
- Jerarquía de figuras: Crea
abstract class FiguraconArea()yPerimetro(). ImplementaCirculo,RectanguloyTriangulo. Crea una lista de figuras y calcula el área total. - Interfaz IExportable: Define una interfaz
IExportablecon métodosExportarCsv()yExportarJson(). Impleméntala en una claseReporte. - Polimorfismo: Crea un método
ImprimirFiguras(IEnumerable<Figura> figuras)que itere e imprima el tipo, área y perímetro de cada figura.
En la siguiente lección exploraremos los genéricos y las colecciones: List
// Clase base (abstracta)
public abstract class Vehiculo
{
public string Marca { get; init; }
public string Modelo { get; init; }
public int Anio { get; init; }
protected Vehiculo(string marca, string modelo, int anio)
{
Marca = marca;
Modelo = modelo;
Anio = anio;
}
// Método abstracto — DEBE implementarse en subclases
public abstract string TipoMotor();
// Método virtual — PUEDE sobreescribirse
public virtual string Describir()
=> $"{Marca} {Modelo} ({Anio}) — {TipoMotor()}";
// Método sellado en subclase
public string Identificador() => $"{Marca}-{Modelo}-{Anio}";
}
// Subclase concreta
public class AutoElectrico : Vehiculo
{
public int CapacidadBateria { get; init; } // kWh
public AutoElectrico(string marca, string modelo, int anio, int bateria)
: base(marca, modelo, anio)
{
CapacidadBateria = bateria;
}
public override string TipoMotor() => "Eléctrico";
// sealed — ninguna subclase puede sobreescribir esto
public sealed override string Describir()
=> base.Describir() + $" | Batería: {CapacidadBateria} kWh";
}
var tesla = new AutoElectrico("Tesla", "Model 3", 2024, 82);
Console.WriteLine(tesla.Describir());
// Tesla Model 3 (2024) — Eléctrico | Batería: 82 kWh
Inicia sesión para guardar tu progreso