CSharp

Tutorial CSharp: Herencia

Aprende la herencia en C# con ejemplos de constructores y uso de base para crear jerarquías de clases eficientes y reutilizables.

Aprende CSharp y certifícate

Concepto y sintaxis

La herencia es uno de los pilares fundamentales de la programación orientada a objetos en C#. Este mecanismo permite crear nuevas clases que reutilizan, extienden y modifican el comportamiento definido en otras clases. La clase de la que se hereda se denomina clase base o clase padre, mientras que la clase que hereda se conoce como clase derivada o clase hija.

La herencia establece una relación "es un" entre clases. Por ejemplo, si tenemos una clase Vehículo y creamos una clase Coche que hereda de ella, estamos indicando que un coche "es un" vehículo.

Sintaxis básica

En C#, la herencia se implementa utilizando el símbolo de dos puntos (:) para indicar que una clase deriva de otra. La sintaxis es la siguiente:

class ClaseDerivada : ClaseBase
{
    // Miembros de la clase derivada
}

Veamos un ejemplo sencillo:

// Clase base
class Animal
{
    public string Nombre { get; set; }
    
    public void Respirar()
    {
        Console.WriteLine("Respirando...");
    }
}

// Clase derivada
class Perro : Animal
{
    public void Ladrar()
    {
        Console.WriteLine("¡Guau guau!");
    }
}

En este ejemplo, la clase Perro hereda todas las propiedades y métodos públicos de la clase Animal. Esto significa que un objeto de tipo Perro tendrá acceso a la propiedad Nombre y al método Respirar(), además de su propio método Ladrar().

Uso de la herencia

Para utilizar la herencia, simplemente creamos instancias de la clase derivada y accedemos tanto a los miembros heredados como a los propios:

Perro miPerro = new Perro();
miPerro.Nombre = "Bobby";  // Propiedad heredada de Animal
miPerro.Respirar();        // Método heredado de Animal
miPerro.Ladrar();          // Método propio de Perro

Modificadores de acceso y herencia

Es importante entender cómo los modificadores de acceso afectan a la herencia:

  • Los miembros public son accesibles desde cualquier parte, incluidas las clases derivadas.
  • Los miembros private solo son accesibles dentro de la propia clase, no en las clases derivadas.
  • Los miembros protected son accesibles dentro de la clase y en todas las clases derivadas.
  • Los miembros internal son accesibles dentro del mismo ensamblado.
  • Los miembros protected internal combinan ambos comportamientos.

Ejemplo con diferentes modificadores de acceso:

class Persona
{
    public string Nombre { get; set; }      // Accesible en cualquier lugar
    private int edad;                        // Solo accesible en Persona
    protected string direccion;              // Accesible en Persona y clases derivadas
    
    public void Saludar()
    {
        Console.WriteLine($"Hola, soy {Nombre}");
    }
    
    private void DatosPrivados()
    {
        Console.WriteLine("Método privado");
    }
    
    protected void MostrarDireccion()
    {
        Console.WriteLine($"Vivo en {direccion}");
    }
}

class Estudiante : Persona
{
    public string Curso { get; set; }
    
    public void MostrarInfo()
    {
        Console.WriteLine($"Estudiante: {Nombre}");  // Acceso a propiedad pública
        // Console.WriteLine($"Edad: {edad}");       // Error: no se puede acceder a miembros privados
        Console.WriteLine($"Dirección: {direccion}"); // Acceso a campo protegido
        MostrarDireccion();                          // Acceso a método protegido
        // DatosPrivados();                          // Error: no se puede acceder a métodos privados
    }
}

Herencia única en C#

C# solo permite la herencia única, lo que significa que una clase puede heredar directamente de una sola clase base. Sin embargo, una clase puede implementar múltiples interfaces (que veremos en lecciones posteriores).

// Esto es válido:
class A { }
class B : A { }

// Esto NO es válido en C#:
// class C : A, B { }  // Error: herencia múltiple no permitida

Jerarquías de herencia

Las clases pueden formar jerarquías de herencia, donde una clase derivada puede a su vez ser la clase base de otra:

class Animal
{
    public void Respirar() { Console.WriteLine("Respirando..."); }
}

class Mamifero : Animal
{
    public void Amamantar() { Console.WriteLine("Amamantando crías..."); }
}

class Perro : Mamifero
{
    public void Ladrar() { Console.WriteLine("¡Guau guau!"); }
}

En este ejemplo, Perro hereda de Mamifero, que a su vez hereda de Animal. Por lo tanto, un objeto Perro tendrá acceso a los métodos Respirar(), Amamantar() y Ladrar().

La clase Object

En C#, todas las clases heredan implícitamente de la clase Object (o System.Object), incluso si no se especifica. Esto significa que todas las clases tienen acceso a los métodos definidos en Object, como ToString(), Equals(), GetHashCode(), etc.

// Estas dos declaraciones son equivalentes:
class MiClase { }
class MiClase : object { }

Sealed: Evitando la herencia

Si queremos evitar que una clase sea heredada, podemos marcarla como sealed:

sealed class ClaseFinal
{
    // Esta clase no puede ser heredada
}

// Esto generaría un error:
// class ClaseDerivada : ClaseFinal { }  // Error: no se puede heredar de una clase sealed

También podemos marcar métodos específicos como sealed en clases derivadas para evitar que sean sobrescritos en futuras derivaciones, pero esto solo se puede hacer en métodos que ya estén sobrescribiendo métodos de la clase base:

class Base
{
    public virtual void Metodo() { }
}

class Derivada : Base
{
    public sealed override void Metodo() { }  // No se podrá sobrescribir en clases que hereden de Derivada
}

La herencia es un mecanismo fundamental en C# que permite crear código más organizado, reutilizable y mantenible. Al comprender correctamente su sintaxis y conceptos, podrás diseñar jerarquías de clases efectivas para tus aplicaciones.

Constructores en herencia

Cuando trabajamos con herencia en C#, uno de los aspectos más importantes a comprender es cómo funcionan los constructores en la cadena de herencia. Los constructores no se heredan, pero juegan un papel fundamental en la inicialización correcta de los objetos derivados.

Orden de ejecución de constructores

En una relación de herencia, los constructores se ejecutan en un orden específico:

  1. Primero se ejecuta el constructor de la clase base
  2. Después se ejecuta el constructor de la clase derivada

Este orden garantiza que la parte base del objeto esté correctamente inicializada antes de inicializar la parte específica de la clase derivada.

class Animal
{
    public Animal()
    {
        Console.WriteLine("Constructor de Animal");
    }
}

class Perro : Animal
{
    public Perro()
    {
        Console.WriteLine("Constructor de Perro");
    }
}

Al crear una instancia de Perro, veremos en la consola:

Constructor de Animal
Constructor de Perro

Constructores con parámetros

Cuando la clase base tiene constructores con parámetros, la clase derivada debe proporcionar los valores necesarios para estos parámetros. Esto se hace utilizando la palabra clave base seguida de los argumentos entre paréntesis:

class Animal
{
    public string Nombre { get; set; }
    
    public Animal(string nombre)
    {
        Nombre = nombre;
        Console.WriteLine($"Animal creado con nombre: {Nombre}");
    }
}

class Perro : Animal
{
    public string Raza { get; set; }
    
    public Perro(string nombre, string raza) : base(nombre)
    {
        Raza = raza;
        Console.WriteLine($"Perro de raza {Raza} creado");
    }
}

Al crear un objeto Perro:

Perro miPerro = new Perro("Bobby", "Labrador");

La salida sería:

Animal creado con nombre: Bobby
Perro de raza Labrador creado

Constructor predeterminado y herencia

Si la clase base no tiene un constructor sin parámetros (constructor predeterminado) y solo define constructores con parámetros, las clases derivadas deben llamar explícitamente a uno de los constructores disponibles de la clase base:

class Vehiculo
{
    public int Ruedas { get; set; }
    
    // Solo tiene constructor con parámetros
    public Vehiculo(int ruedas)
    {
        Ruedas = ruedas;
    }
}

class Coche : Vehiculo
{
    // Debe llamar explícitamente al constructor de la clase base
    public Coche() : base(4)
    {
        // Inicialización específica de Coche
    }
    
    // Otra sobrecarga que permite especificar el número de ruedas
    public Coche(int ruedas) : base(ruedas)
    {
        // Inicialización específica de Coche
    }
}

Si no proporcionamos la llamada al constructor base, obtendremos un error de compilación:

'Vehiculo' no contiene un constructor que tome 0 argumentos

Constructores en jerarquías de herencia multinivel

En jerarquías de herencia con múltiples niveles, los constructores se ejecutan en secuencia desde la clase base más alta hasta la clase derivada más baja:

class Animal
{
    public Animal()
    {
        Console.WriteLine("Constructor de Animal");
    }
}

class Mamifero : Animal
{
    public Mamifero()
    {
        Console.WriteLine("Constructor de Mamifero");
    }
}

class Perro : Mamifero
{
    public Perro()
    {
        Console.WriteLine("Constructor de Perro");
    }
}

Al crear una instancia de Perro, la salida sería:

Constructor de Animal
Constructor de Mamifero
Constructor de Perro

Inicializadores de objeto con herencia

Los inicializadores de objeto también funcionan con clases derivadas, permitiendo establecer propiedades tanto de la clase base como de la derivada en una sola expresión:

class Persona
{
    public string Nombre { get; set; }
    public int Edad { get; set; }
}

class Estudiante : Persona
{
    public string Curso { get; set; }
    public double Promedio { get; set; }
}

// Creación e inicialización en una sola expresión
Estudiante estudiante = new Estudiante
{
    Nombre = "Ana",     // Propiedad de la clase base
    Edad = 20,          // Propiedad de la clase base
    Curso = "Informática", // Propiedad de la clase derivada
    Promedio = 8.5      // Propiedad de la clase derivada
};

Constructores estáticos en herencia

Los constructores estáticos también siguen un orden específico en la herencia. El constructor estático de la clase base se ejecuta antes que el constructor estático de la clase derivada, pero ambos se ejecutan antes que cualquier constructor de instancia:

class Base
{
    static Base()
    {
        Console.WriteLine("Constructor estático de Base");
    }
    
    public Base()
    {
        Console.WriteLine("Constructor de instancia de Base");
    }
}

class Derivada : Base
{
    static Derivada()
    {
        Console.WriteLine("Constructor estático de Derivada");
    }
    
    public Derivada()
    {
        Console.WriteLine("Constructor de instancia de Derivada");
    }
}

Al crear el primer objeto Derivada, la salida sería:

Constructor estático de Base
Constructor estático de Derivada
Constructor de instancia de Base
Constructor de instancia de Derivada

Constructores privados y herencia

Si una clase base tiene solo constructores privados, no puede ser heredada, ya que las clases derivadas no podrían llamar al constructor de la clase base:

class ClaseNoHeredable
{
    private ClaseNoHeredable()
    {
        // Constructor privado
    }
    
    // Método de fábrica para crear instancias
    public static ClaseNoHeredable Crear()
    {
        return new ClaseNoHeredable();
    }
}

// Esto generaría un error de compilación:
// class ClaseDerivada : ClaseNoHeredable { }

Esta técnica se utiliza a veces como alternativa a marcar una clase como sealed para evitar la herencia.

Ejemplo práctico: Sistema de formas geométricas

Veamos un ejemplo más completo que ilustra el uso de constructores en herencia:

class Forma
{
    public string Color { get; set; }
    
    public Forma()
    {
        Color = "Blanco"; // Color predeterminado
    }
    
    public Forma(string color)
    {
        Color = color;
    }
    
    public virtual void Dibujar()
    {
        Console.WriteLine($"Dibujando una forma de color {Color}");
    }
}

class Circulo : Forma
{
    public double Radio { get; set; }
    
    // Constructor que usa el constructor predeterminado de la clase base
    public Circulo(double radio)
    {
        Radio = radio;
    }
    
    // Constructor que especifica el color
    public Circulo(double radio, string color) : base(color)
    {
        Radio = radio;
    }
    
    public override void Dibujar()
    {
        Console.WriteLine($"Dibujando un círculo de color {Color} con radio {Radio}");
    }
}

class Rectangulo : Forma
{
    public double Ancho { get; set; }
    public double Alto { get; set; }
    
    public Rectangulo(double ancho, double alto) : base()
    {
        Ancho = ancho;
        Alto = alto;
    }
    
    public Rectangulo(double ancho, double alto, string color) : base(color)
    {
        Ancho = ancho;
        Alto = alto;
    }
    
    public override void Dibujar()
    {
        Console.WriteLine($"Dibujando un rectángulo de color {Color} de {Ancho}x{Alto}");
    }
}

Uso de estas clases:

// Usando constructores diferentes
Forma forma = new Forma();
Circulo circulo1 = new Circulo(5.0);
Circulo circulo2 = new Circulo(3.0, "Rojo");
Rectangulo rectangulo = new Rectangulo(4.0, 6.0, "Azul");

// Dibujando las formas
forma.Dibujar();
circulo1.Dibujar();
circulo2.Dibujar();
rectangulo.Dibujar();

Salida:

Dibujando una forma de color Blanco
Dibujando un círculo de color Blanco con radio 5
Dibujando un círculo de color Rojo con radio 3
Dibujando un rectángulo de color Azul de 4x6

Entender cómo funcionan los constructores en la herencia es esencial para crear jerarquías de clases correctamente inicializadas y mantener la integridad de los objetos en aplicaciones orientadas a objetos.

Uso de base

La palabra clave base en C# es una herramienta fundamental cuando trabajamos con herencia, ya que nos permite acceder a los miembros de la clase base desde una clase derivada. Esta referencia es especialmente útil cuando necesitamos utilizar la implementación de la clase padre mientras añadimos funcionalidad adicional en la clase hija.

Acceso a métodos de la clase base

Cuando una clase derivada sobrescribe un método de la clase base, a veces necesitamos ejecutar el código del método original antes o después de nuestro código personalizado. La palabra clave base nos permite hacer exactamente eso:

class Animal
{
    public virtual void HacerSonido()
    {
        Console.WriteLine("El animal hace un sonido");
    }
}

class Gato : Animal
{
    public override void HacerSonido()
    {
        // Llamamos al método de la clase base primero
        base.HacerSonido();
        
        // Añadimos comportamiento específico
        Console.WriteLine("Miau!");
    }
}

Al ejecutar este código:

Gato miGato = new Gato();
miGato.HacerSonido();

Obtendremos:

El animal hace un sonido
Miau!

Este enfoque es muy útil cuando queremos extender el comportamiento de un método en lugar de reemplazarlo completamente.

Acceso a propiedades y campos de la clase base

De manera similar, podemos usar base para acceder a propiedades y campos de la clase base:

class Empleado
{
    protected string nombre;
    public virtual string Informacion => $"Empleado: {nombre}";
}

class Gerente : Empleado
{
    private string departamento;
    
    public Gerente(string nombre, string departamento)
    {
        this.nombre = nombre;  // Accedemos al campo protegido de la clase base
        this.departamento = departamento;
    }
    
    public override string Informacion => $"{base.Informacion}, Departamento: {departamento}";
}

En este ejemplo, la propiedad Informacion de la clase Gerente utiliza base.Informacion para obtener la implementación de la clase base y luego la extiende con información adicional.

Resolución de ambigüedad de nombres

Cuando una clase derivada tiene miembros con el mismo nombre que los de la clase base, base ayuda a resolver esta ambigüedad:

class Producto
{
    public string Nombre { get; set; }
    
    public virtual decimal CalcularPrecio()
    {
        return 0;
    }
}

class ProductoElectronico : Producto
{
    // Propiedad con el mismo nombre que en la clase base
    public new string Nombre { get; set; }
    
    public string Modelo { get; set; }
    
    public void MostrarDetalles()
    {
        // Accedemos a ambas propiedades Nombre
        Console.WriteLine($"Nombre del producto: {base.Nombre}");
        Console.WriteLine($"Nombre comercial: {Nombre}");
        Console.WriteLine($"Modelo: {Modelo}");
    }
    
    public override decimal CalcularPrecio()
    {
        // Usamos el precio base y añadimos un cargo adicional
        decimal precioBase = base.CalcularPrecio();
        return precioBase + 100; // Cargo adicional por ser electrónico
    }
}

Uso de base con propiedades indexadas

También podemos usar base para acceder a indexadores de la clase base:

class ColeccionBase
{
    protected string[] elementos = new string[10];
    
    public virtual string this[int indice]
    {
        get { return elementos[indice]; }
        set { elementos[indice] = value; }
    }
}

class ColeccionEspecial : ColeccionBase
{
    public override string this[int indice]
    {
        get { return $"Elemento: {base[indice]}"; }
        set { base[indice] = value.ToUpper(); }
    }
}

Uso de base con operadores

Si sobrecargamos operadores en una jerarquía de clases, también podemos usar base para acceder a la implementación de la clase base:

class Valor
{
    public int Numero { get; set; }
    
    public static Valor operator +(Valor a, Valor b)
    {
        return new Valor { Numero = a.Numero + b.Numero };
    }
}

class ValorEspecial : Valor
{
    public string Unidad { get; set; }
    
    public static new ValorEspecial operator +(ValorEspecial a, ValorEspecial b)
    {
        // Usamos el operador de la clase base para la suma
        Valor resultado = base.operator +(a, b);
        
        return new ValorEspecial 
        { 
            Numero = resultado.Numero,
            Unidad = a.Unidad // Mantenemos la unidad del primer operando
        };
    }
}

Nota: El ejemplo anterior es conceptual, ya que C# no permite llamar directamente a operadores de la clase base con la sintaxis mostrada. En la práctica, tendríamos que convertir los objetos al tipo base o implementar la lógica de suma nuevamente.

Ejemplo práctico: Sistema de notificaciones

Veamos un ejemplo más completo que ilustra el uso de base en un sistema de notificaciones:

class Notificacion
{
    public string Remitente { get; set; }
    public string Destinatario { get; set; }
    public string Mensaje { get; set; }
    
    public Notificacion(string remitente, string destinatario, string mensaje)
    {
        Remitente = remitente;
        Destinatario = destinatario;
        Mensaje = mensaje;
    }
    
    public virtual void Enviar()
    {
        Console.WriteLine($"Enviando mensaje de {Remitente} a {Destinatario}");
        Console.WriteLine($"Contenido: {Mensaje}");
    }
}

class NotificacionUrgente : Notificacion
{
    public int Prioridad { get; set; }
    
    public NotificacionUrgente(string remitente, string destinatario, string mensaje, int prioridad) 
        : base(remitente, destinatario, mensaje)
    {
        Prioridad = prioridad;
    }
    
    public override void Enviar()
    {
        Console.WriteLine($"NOTIFICACIÓN URGENTE (Prioridad: {Prioridad})");
        base.Enviar();
        Console.WriteLine("Se ha enviado una copia a servicios de emergencia");
    }
}

class NotificacionProgramada : Notificacion
{
    public DateTime FechaEnvio { get; set; }
    
    public NotificacionProgramada(string remitente, string destinatario, string mensaje, DateTime fechaEnvio) 
        : base(remitente, destinatario, mensaje)
    {
        FechaEnvio = fechaEnvio;
    }
    
    public override void Enviar()
    {
        if (DateTime.Now >= FechaEnvio)
        {
            base.Enviar();
        }
        else
        {
            Console.WriteLine($"La notificación se enviará el {FechaEnvio}");
        }
    }
}

Uso de estas clases:

Notificacion notificacion = new Notificacion("Sistema", "Usuario", "Mensaje normal");
NotificacionUrgente urgente = new NotificacionUrgente("Sistema", "Administrador", "¡Servidor caído!", 1);
NotificacionProgramada programada = new NotificacionProgramada("Marketing", "Clientes", "Oferta especial", DateTime.Now.AddDays(1));

notificacion.Enviar();
Console.WriteLine();
urgente.Enviar();
Console.WriteLine();
programada.Enviar();

Limitaciones del uso de base

Es importante entender algunas limitaciones al usar base:

  • Solo se puede acceder a miembros de la clase base inmediata, no a clases más arriba en la jerarquía.
  • No se puede usar base para acceder a miembros private de la clase base.
  • La palabra clave base no se puede usar en métodos estáticos.
class A
{
    protected void MetodoA() { }
}

class B : A
{
    protected void MetodoB() { }
}

class C : B
{
    public void Metodo()
    {
        base.MetodoB();    // Correcto: accede a un método de la clase base inmediata (B)
        // base.MetodoA(); // Error: no puede acceder directamente a métodos de A
        
        // Para acceder a MetodoA, tendríamos que hacerlo a través de B
        ((A)this).MetodoA(); // Esto funcionaría si MetodoA fuera público o protegido
    }
}

Uso de base en constructores vs. métodos

Es importante distinguir entre el uso de base en constructores y en métodos:

  • En constructores, base(...) se usa para llamar a un constructor específico de la clase base.
  • En métodos, base.Metodo() se usa para llamar a la implementación del método en la clase base.
class Figura
{
    public string Color { get; }
    
    public Figura(string color)
    {
        Color = color;
    }
    
    public virtual void Dibujar()
    {
        Console.WriteLine($"Dibujando una figura de color {Color}");
    }
}

class Triangulo : Figura
{
    public double Base { get; }
    public double Altura { get; }
    
    // Uso de base en constructor
    public Triangulo(double baseTriangulo, double altura, string color) : base(color)
    {
        Base = baseTriangulo;
        Altura = altura;
    }
    
    // Uso de base en método
    public override void Dibujar()
    {
        base.Dibujar();
        Console.WriteLine($"Es un triángulo con base {Base} y altura {Altura}");
    }
}

La palabra clave base es una herramienta esencial en C# que facilita la creación de jerarquías de clases bien estructuradas, permitiendo que las clases derivadas extiendan el comportamiento de sus clases base de manera controlada y efectiva.

Aprende CSharp online

Otras lecciones de CSharp

Accede a todas las lecciones de CSharp y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Introducción A C#

Introducción Y Entorno

Creación De Proyecto C#

Introducción Y Entorno

Variables Y Constantes

Sintaxis

Tipos De Datos

Sintaxis

Operadores

Sintaxis

Control De Flujo

Sintaxis

Funciones

Sintaxis

Estructuras De Control Iterativo

Sintaxis

Interpolación De Strings

Sintaxis

Estructuras De Control Condicional

Sintaxis

Manejo De Valores Nulos

Sintaxis

Clases Y Encapsulación

Programación Orientada A Objetos

Objetos

Programación Orientada A Objetos

Constructores Y Destructores

Programación Orientada A Objetos

Herencia

Programación Orientada A Objetos

Polimorfismo

Programación Orientada A Objetos

Genéricos

Programación Orientada A Objetos

Métodos Virtuales Y Sobrecarga

Programación Orientada A Objetos

Clases Abstractas

Programación Orientada A Objetos

Interfaces

Programación Orientada A Objetos

Propiedades Y Encapsulación

Programación Orientada A Objetos

Métodos De Extensión

Programación Orientada A Objetos

Clases Y Objetos

Programación Orientada A Objetos

Clases Parciales

Programación Orientada A Objetos

Miembros Estáticos

Programación Orientada A Objetos

Tuplas Y Tipos Anónimos

Programación Orientada A Objetos

Arrays Y Listas

Colecciones Y Linq

Diccionarios

Colecciones Y Linq

Conjuntos, Colas Y Pilas

Colecciones Y Linq

Uso De Consultas Linq

Colecciones Y Linq

Linq Avanzado

Colecciones Y Linq

Colas Y Pilas

Colecciones Y Linq

Conjuntos

Colecciones Y Linq

Linq Básico

Colecciones Y Linq

Delegados Funcionales

Programación Funcional

Records

Programación Funcional

Expresiones Lambda

Programación Funcional

Linq Funcional

Programación Funcional

Fundamentos De La Programación Funcional

Programación Funcional

Pattern Matching

Programación Funcional

Testing Unitario Con Xunit

Testing

Excepciones

Excepciones

Delegados

Programación Asíncrona

Eventos

Programación Asíncrona

Lambdas

Programación Asíncrona

Uso De Async Y Await

Programación Asíncrona

Tareas

Programación Asíncrona

Accede GRATIS a CSharp y certifícate

Ejercicios de programación de CSharp

Evalúa tus conocimientos de esta lección Herencia con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender el concepto y la sintaxis básica de la herencia en C#.
  • Identificar cómo funcionan los modificadores de acceso en el contexto de la herencia.
  • Entender el orden y uso de constructores en jerarquías de herencia.
  • Aprender a utilizar la palabra clave base para acceder a miembros y constructores de la clase base.
  • Reconocer las limitaciones y buenas prácticas al trabajar con herencia en C#.