Módulo 12: Herencia

Objetivos del módulo

  • Comprender el concepto de herencia
  • Crear jerarquías de clases con :
  • Usar virtual y override para modificar comportamientos
  • Entender base para acceder a la clase padre
  • Conocer clases abstract y sealed

1. ¿Qué es la herencia?

📘 Concepto: La herencia permite crear una clase nueva (clase hija o derivada) basada en una clase existente (clase padre o base). La clase hija hereda todas las propiedades y métodos de la padre, y puede añadir nuevos o modificar los existentes.

El problema sin herencia

// Sin herencia: hay código repetido en cada clase
class Perro { public string Nombre; public int Edad; public void Comer() { } public void Ladrar() { } }
class Gato  { public string Nombre; public int Edad; public void Comer() { } public void Maullar() { } }
class Pez   { public string Nombre; public int Edad; public void Comer() { } public void Nadar() { } }
// Nombre, Edad y Comer() están repetidos en las 3 clases

La solución con herencia

// Clase base: lo que tienen en común
class Animal
{
    public string Nombre { get; set; } = "";
    public int Edad { get; set; }

    public void Comer()
    {
        Console.WriteLine($"{Nombre} está comiendo.");
    }
}

// Clases derivadas: heredan todo y añaden lo específico
class Perro : Animal  // ← Perro hereda de Animal
{
    public string Raza { get; set; } = "";

    public void Ladrar()
    {
        Console.WriteLine($"{Nombre} dice: ¡Guau!");
    }
}

class Gato : Animal
{
    public bool EsDeInterior { get; set; }

    public void Maullar()
    {
        Console.WriteLine($"{Nombre} dice: ¡Miau!");
    }
}

Uso

Perro perro = new Perro
{
    Nombre = "Rex",       // Heredado de Animal
    Edad = 3,             // Heredado de Animal
    Raza = "Pastor alemán" // Propio de Perro
};

perro.Comer();   // Heredado de Animal: "Rex está comiendo."
perro.Ladrar();  // Propio de Perro: "Rex dice: ¡Guau!"

Gato gato = new Gato { Nombre = "Luna", Edad = 2, EsDeInterior = true };
gato.Comer();    // Heredado de Animal
gato.Maullar();  // Propio de Gato

2. virtual y override: modificar comportamiento

📘 Concepto: Si la clase padre marca un método como virtual, las clases hijas pueden sobrescribirlo con override para cambiar su comportamiento.

class Empleado
{
    public string Nombre { get; set; } = "";
    public double SalarioBase { get; set; }

    // virtual: las clases hijas PUEDEN sobrescribir este método
    public virtual double CalcularSalario()
    {
        return SalarioBase;
    }

    public override string ToString()
    {
        return $"{Nombre}: {CalcularSalario():C2}";
    }
}

class EmpleadoConComision : Empleado
{
    public double Ventas { get; set; }
    public double PorcentajeComision { get; set; }

    // override: sobrescribe el método del padre
    public override double CalcularSalario()
    {
        return SalarioBase + (Ventas * PorcentajeComision / 100);
    }
}

class Directivo : Empleado
{
    public double BonoAnual { get; set; }

    public override double CalcularSalario()
    {
        return SalarioBase + (BonoAnual / 12);  // Bono prorrateado mensual
    }
}

// Uso
var emp = new Empleado { Nombre = "Ana", SalarioBase = 2000 };
var com = new EmpleadoConComision
{
    Nombre = "Luis",
    SalarioBase = 1500,
    Ventas = 50000,
    PorcentajeComision = 5
};
var dir = new Directivo { Nombre = "María", SalarioBase = 4000, BonoAnual = 12000 };

Console.WriteLine(emp);  // Ana: 2.000,00 €
Console.WriteLine(com);  // Luis: 4.000,00 €  (1500 + 50000*5%)
Console.WriteLine(dir);  // María: 5.000,00 €  (4000 + 12000/12)

3. base: acceder a la clase padre

La palabra base permite acceder a los miembros de la clase padre:

Llamar al constructor del padre

class Vehiculo
{
    public string Marca { get; set; }
    public string Modelo { get; set; }
    public int Año { get; set; }

    public Vehiculo(string marca, string modelo, int año)
    {
        Marca = marca;
        Modelo = modelo;
        Año = año;
    }
}

class Coche : Vehiculo
{
    public int Puertas { get; set; }

    // Llamamos al constructor del padre con : base(...)
    public Coche(string marca, string modelo, int año, int puertas)
        : base(marca, modelo, año)  // ← Pasar datos al constructor de Vehiculo
    {
        Puertas = puertas;
    }
}

var coche = new Coche("Toyota", "Corolla", 2024, 4);

Llamar a un método del padre

class Animal
{
    public virtual void HacerSonido()
    {
        Console.WriteLine("(sonido genérico)");
    }
}

class Perro : Animal
{
    public override void HacerSonido()
    {
        base.HacerSonido();  // Ejecuta el método del padre primero
        Console.WriteLine("¡Guau guau!");
    }
}

new Perro().HacerSonido();
// (sonido genérico)
// ¡Guau guau!

4. Clases abstractas (abstract)

📘 Concepto: Una clase abstract no se puede instanciar (no puedes crear objetos de ella). Sirve como plantilla para clases hijas. Sus métodos abstract obligan a las hijas a implementarlos.

abstract class Figura
{
    public string Color { get; set; } = "Negro";

    // Método abstracto: NO tiene cuerpo, las hijas DEBEN implementarlo
    public abstract double CalcularArea();
    public abstract double CalcularPerimetro();

    // Método normal: las hijas lo heredan tal cual
    public void MostrarInfo()
    {
        Console.WriteLine($"Figura: {GetType().Name}");
        Console.WriteLine($"Color: {Color}");
        Console.WriteLine($"Área: {CalcularArea():F2}");
        Console.WriteLine($"Perímetro: {CalcularPerimetro():F2}");
    }
}

class Circulo : Figura
{
    public double Radio { get; set; }

    public Circulo(double radio) { Radio = radio; }

    // OBLIGATORIO: implementar los métodos abstractos
    public override double CalcularArea() => Math.PI * Radio * Radio;
    public override double CalcularPerimetro() => 2 * Math.PI * Radio;
}

class Rectangulo : Figura
{
    public double Ancho { get; set; }
    public double Alto { get; set; }

    public Rectangulo(double ancho, double alto) { Ancho = ancho; Alto = alto; }

    public override double CalcularArea() => Ancho * Alto;
    public override double CalcularPerimetro() => 2 * (Ancho + Alto);
}

class Triangulo : Figura
{
    public double Base { get; set; }
    public double Altura { get; set; }
    public double Lado1 { get; set; }
    public double Lado2 { get; set; }
    public double Lado3 { get; set; }

    public override double CalcularArea() => Base * Altura / 2;
    public override double CalcularPerimetro() => Lado1 + Lado2 + Lado3;
}

// Uso
// Figura f = new Figura();  // ERROR: no se puede instanciar una clase abstracta

Figura circulo = new Circulo(5) { Color = "Rojo" };
Figura rect = new Rectangulo(4, 6) { Color = "Azul" };

circulo.MostrarInfo();
rect.MostrarInfo();

// Polimorfismo: una lista de diferentes figuras
List<Figura> figuras = new() { circulo, rect, new Triangulo
{
    Base = 3, Altura = 4, Lado1 = 3, Lado2 = 4, Lado3 = 5
}};

double areaTotal = 0;
foreach (Figura f in figuras)
{
    areaTotal += f.CalcularArea();  // Cada figura calcula su propia área
}
Console.WriteLine($"Área total: {areaTotal:F2}");

5. Clases selladas (sealed)

📘 Concepto: Una clase sealed no puede ser heredada. Se usa cuando quieres evitar que alguien extienda tu clase.

sealed class ConfiguracionApp
{
    public string NombreApp { get; set; } = "MiApp";
    public string Version { get; set; } = "1.0";

    // Singleton: una sola instancia
    private static ConfiguracionApp? _instancia;
    public static ConfiguracionApp Instancia => _instancia ??= new ConfiguracionApp();

    private ConfiguracionApp() { }  // Constructor privado
}

// class MiConfig : ConfiguracionApp { }  // ERROR: no se puede heredar de sealed

var config = ConfiguracionApp.Instancia;
Console.WriteLine(config.NombreApp);

6. Operador is y casting

Puedes comprobar si un objeto es de cierto tipo y convertirlo:

Animal miAnimal = new Perro { Nombre = "Rex", Raza = "Labrador" };

// Comprobar tipo con 'is'
if (miAnimal is Perro perro)
{
    Console.WriteLine($"Es un perro de raza {perro.Raza}");
}

// Casting explícito
if (miAnimal is Perro)
{
    Perro p = (Perro)miAnimal;  // Cast directo (lanza excepción si no es Perro)
}

// Casting con 'as' (devuelve null si no es compatible)
Gato? gato = miAnimal as Gato;  // null porque miAnimal es Perro, no Gato
if (gato is not null)
{
    Console.WriteLine($"Es un gato: {gato.Nombre}");
}

7. Herencia y ToString()

class Animal
{
    public string Nombre { get; set; } = "";
    public override string ToString() => $"{GetType().Name}: {Nombre}";
}

class Perro : Animal
{
    public string Raza { get; set; } = "";
    public override string ToString() => $"{base.ToString()}, Raza: {Raza}";
}

var p = new Perro { Nombre = "Rex", Raza = "Labrador" };
Console.WriteLine(p);  // "Perro: Rex, Raza: Labrador"

8. Ejercicios

Ejercicio 1: Jerarquía de vehículos

Crea una jerarquía:

  • Vehiculo (base): marca, modelo, año, velocidad actual, Acelerar(), Frenar()
  • Coche : puertas, tipo combustible
  • Moto : cilindrada
  • CocheElectrico : nivel batería, Recargar()

Todos sobrescriben ToString(). Crea un garaje (lista de vehículos) y muestra todos.

Ejercicio 2: Figuras geométricas

Expande el ejemplo de figuras: añade Cuadrado (hereda de Rectangulo), Elipse, Trapecio. Calcula el área total de una lista de figuras mezcladas.

Ejercicio 3: Sistema de empleados

Clase base Empleado (nombre, ID, salario base). Subclases: EmpleadoFijo (+antigüedad, bono por año), EmpleadoTemporal (+fecha fin contrato), Directivo (+bono, empleados a cargo). Calcula la nómina total de la empresa.

Ejercicio 4: RPG con herencia

Crea un mini juego de rol: Personaje (base) con nombre, vida, ataque. Subclases: Guerrero (golpe fuerte, defensa alta), Mago (hechizo, maná), Arquero (flecha, precisión). Implementa combate por turnos entre dos personajes.


Resumen

Concepto Sintaxis
Herencia class Hija : Padre { }
Método virtual public virtual void Metodo() { }
Sobrescribir public override void Metodo() { }
Llamar al padre base.Metodo()
Constructor padre : base(params)
Clase abstracta abstract class Base { }
Método abstracto public abstract void Metodo();
Clase sellada sealed class Final { }
Comprobar tipo obj is Tipo variable
Casting (Tipo)obj o obj as Tipo