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ícateConcepto 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:
- Primero se ejecuta el constructor de la clase base
- 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.
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
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.
CRUD en C# de modelo Customer sobre una lista
Arrays y listas
Objetos
Excepciones
Eventos
Lambdas
Diccionarios en C#
Variables y constantes
Tipos de datos
Herencia
Operadores
Uso de consultas LINQ
Clases y encapsulación
Uso de consultas LINQ
Excepciones
Control de flujo
Eventos
Diccionarios
Tipos de datos
Conjuntos, colas y pilas
Lambdas
Conjuntos, colas y pilas
Uso de async y await
Tareas
Constructores y destructores
Operadores
Arrays y listas
Polimorfismo
Polimorfismo
Variables y constantes
Proyecto colecciones y LINQ en C#
Clases y encapsulación
Creación de proyecto C#
Uso de async y await
Funciones
Delegados
Delegados
Constructores y destructores
Objetos
Control de flujo
Funciones
Tareas
Proyecto sintaxis en C#
Herencia C Sharp
OOP en C Sharp
Diccionarios
Introducción a C#
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#.