Al cerrar la lección anterior quedó pendiente un problema: Libro ya tiene Titulo, Autor,
Disponible, Prestar(), Devolver() y MostrarFicha() perfectamente definidos, pero si
BiblioTech quisiera incorporar también revistas al catálogo, tendrías que crear una clase
Revista copiando y pegando exactamente esas mismas propiedades y métodos, cambiando solo lo
específico de una revista (por ejemplo, su número de edición). Esa duplicación es justo lo que
la herencia evita: permite definir una clase base con lo que varios tipos tienen en común, y
que otras clases hereden automáticamente esas propiedades y métodos, añadiendo solo lo que
las distingue. En esta lección extraerás de Libro una clase base, MaterialBibliotecario,
de la que heredarán tanto Libro como una nueva clase Revista.
Contenido
- El problema: propiedades y métodos duplicados
- Heredar con
:— la clase baseMaterialBibliotecario Librohereda deMaterialBibliotecario- El operador
basepara llamar al constructor de la clase base - Añadiendo
Revistacomo segunda clase heredera - Sobreescribir miembros heredados con
virtualyoverride - Clases selladas con
sealed - Jerarquía de una sola herencia en C#
- El problema: propiedades y métodos duplicados
Recupera la clase Libro tal y como quedó en la lección anterior:
class Libro
{
public string Titulo { get; set; }
public string Autor { get; set; }
public string Isbn { get; set; }
public bool Disponible { get; set; } = true;
public Libro(string titulo, string autor, string isbn)
{
Titulo = titulo;
Autor = autor;
Isbn = isbn;
}
public void Prestar() { /* ... */ }
public void Devolver() { /* ... */ }
public void MostrarFicha() { /* ... */ }
}Si ahora escribieras una clase Revista desde cero, con Titulo, Autor, Disponible,
Prestar(), Devolver() y MostrarFicha() prácticamente idénticos, y solo Isbn sustituido
por, por ejemplo, NumeroEdicion, estarías duplicando una cantidad considerable de código. Y
lo que es peor: si más adelante corriges un error en Prestar(), tendrías que acordarte de
corregirlo también en la copia de Revista. La herencia resuelve este problema de raíz.
- Heredar con
: — la clase base MaterialBibliotecario
: — la clase base MaterialBibliotecarioEl primer paso es identificar qué tienen en común un libro y una revista dentro de BiblioTech:
ambos tienen un título, un autor, y pueden estar disponibles o prestados. Esa parte común se
extrae a una nueva clase, MaterialBibliotecario, que actuará como clase base:
class MaterialBibliotecario
{
public string Titulo { get; set; }
public string Autor { get; set; }
public bool Disponible { get; set; } = true;
public MaterialBibliotecario(string titulo, string autor)
{
Titulo = titulo;
Autor = autor;
}
public void Prestar()
{
if (Disponible)
{
Disponible = false;
Console.WriteLine($"'{Titulo}' ha sido prestado.");
}
else
{
Console.WriteLine($"'{Titulo}' no esta disponible para prestamo.");
}
}
public void Devolver()
{
Disponible = true;
Console.WriteLine($"'{Titulo}' ha sido devuelto.");
}
public void MostrarFicha()
{
Console.WriteLine($"Titulo: {Titulo}");
Console.WriteLine($"Autor: {Autor}");
Console.WriteLine($"Disponible: {Disponible}");
}
}MaterialBibliotecario no es un concepto que vaya a usarse "en solitario" en BiblioTech —nunca
crearás un objeto que sea "un material bibliotecario" a secas, sin más—, sino la base sobre la
que se construyen conceptos más concretos como Libro o Revista. Esta idea se retomará en la
lección de Abstracción, más adelante en el módulo.
Libro hereda de MaterialBibliotecario
Libro hereda de MaterialBibliotecarioPara que una clase herede de otra, se escribe dos puntos (:) después del nombre de la clase,
seguido del nombre de la clase base:
class Libro : MaterialBibliotecario
{
public string Isbn { get; set; }
public Libro(string titulo, string autor, string isbn) : base(titulo, autor)
{
Isbn = isbn;
}
}class Libro : MaterialBibliotecario significa "Libro es un MaterialBibliotecario,
además de tener sus propias particularidades". Gracias a la herencia, Libro obtiene
automáticamente todas las propiedades (Titulo, Autor, Disponible) y métodos (Prestar(),
Devolver(), MostrarFicha()) de MaterialBibliotecario, sin volver a escribirlos, y solo
necesita declarar lo que le es propio: Isbn.
Libro libro1 = new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
Console.WriteLine(libro1.Titulo); // Rayuela (heredado de MaterialBibliotecario)
Console.WriteLine(libro1.Isbn); // 978-84-376-0495-4 (propio de Libro)
libro1.Prestar(); // 'Rayuela' ha sido prestado. (heredado)
libro1.MostrarFicha(); // heredado, muestra Titulo/Autor/Disponible
- El operador
base para llamar al constructor de la clase base
base para llamar al constructor de la clase baseEn el constructor de Libro del apartado anterior aparece : base(titulo, autor) después de
la firma del constructor. La palabra clave base permite invocar explícitamente un
miembro de la clase base —en este caso, su constructor— desde la clase heredera.
Esto es necesario porque MaterialBibliotecario exige titulo y autor en su propio
constructor (no tiene un constructor sin parámetros), así que cualquier clase que herede de
ella debe indicar explícitamente cómo se construye esa parte heredada. El orden de ejecución es
siempre: primero se ejecuta el constructor de la clase base (MaterialBibliotecario(titulo, autor), que asigna Titulo y Autor), y después el cuerpo del constructor de Libro
(que asigna Isbn).
base también se puede usar dentro de un método para invocar la versión de la clase base de
ese mismo método (lo verás en el apartado 6, con MostrarFicha()), no solo en constructores.
- Añadiendo
Revista como segunda clase heredera
Revista como segunda clase herederaCon MaterialBibliotecario ya definida, añadir Revista al catálogo de BiblioTech es ahora
mucho más sencillo: solo hace falta lo que la distingue de un libro, que en este caso será su
número de edición.
class Revista : MaterialBibliotecario
{
public int NumeroEdicion { get; set; }
public Revista(string titulo, string autor, int numeroEdicion) : base(titulo, autor)
{
NumeroEdicion = numeroEdicion;
}
}Revista revista1 = new Revista("National Geographic", "Varios autores", 302);
revista1.MostrarFicha(); // heredado: Titulo, Autor, Disponible
Console.WriteLine(revista1.NumeroEdicion); // 302 (propio de Revista)
revista1.Prestar(); // heredado, funciona igual que en LibroLibro y Revista comparten toda la lógica de Titulo, Autor, Disponible, Prestar() y
Devolver() sin haberla escrito dos veces: ambas heredan de la misma clase base
MaterialBibliotecario, y cada una añade únicamente su particularidad (Isbn en un caso,
NumeroEdicion en el otro).
- Sobreescribir miembros heredados con
virtual y override
virtual y overridePor defecto, un método heredado se comporta exactamente igual en la clase heredera que en la
clase base: libro1.MostrarFicha() y revista1.MostrarFicha() ejecutan literalmente el mismo
código de MaterialBibliotecario. Pero tiene sentido que la ficha de un libro incluya también
su ISBN, y la de una revista, su número de edición —información que MaterialBibliotecario no
conoce. Para permitir que una clase heredera sobreescriba el comportamiento de un método
heredado, la clase base debe marcar ese método como virtual, y la heredera debe marcar su
propia versión como override:
class MaterialBibliotecario
{
// ... propiedades y constructor como antes ...
public virtual void MostrarFicha()
{
Console.WriteLine($"Titulo: {Titulo}");
Console.WriteLine($"Autor: {Autor}");
Console.WriteLine($"Disponible: {Disponible}");
}
}
class Libro : MaterialBibliotecario
{
public string Isbn { get; set; }
public Libro(string titulo, string autor, string isbn) : base(titulo, autor)
{
Isbn = isbn;
}
public override void MostrarFicha()
{
base.MostrarFicha(); // ejecuta primero la version de MaterialBibliotecario
Console.WriteLine($"ISBN: {Isbn}"); // y anade la linea propia de Libro
}
}Libro libro1 = new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
libro1.MostrarFicha();
// Titulo: Rayuela
// Autor: Julio Cortazar
// Disponible: True
// ISBN: 978-84-376-0495-4Dentro de Libro.MostrarFicha(), la llamada base.MostrarFicha() ejecuta la versión original
del método definida en MaterialBibliotecario (las tres primeras líneas), y a continuación se
añade la línea específica del ISBN. Esto evita duplicar las líneas de Titulo, Autor y
Disponible: se reutiliza la implementación de la clase base y solo se añade lo nuevo.
Revista haría exactamente lo mismo con su propio NumeroEdicion:
class Revista : MaterialBibliotecario
{
public int NumeroEdicion { get; set; }
public Revista(string titulo, string autor, int numeroEdicion) : base(titulo, autor)
{
NumeroEdicion = numeroEdicion;
}
public override void MostrarFicha()
{
base.MostrarFicha();
Console.WriteLine($"Numero de edicion: {NumeroEdicion}");
}
}| Palabra clave | Dónde se usa | Qué significa |
|---|---|---|
virtual |
En el método de la clase base | "Las clases herederas pueden sobreescribir este método" |
override |
En el método de la clase heredera | "Esta es una versión propia de un método marcado virtual en la base" |
base.Metodo() |
Dentro de un método sobreescrito | "Ejecuta también la versión original del método, definida en la clase base" |
Esta lección se ha centrado en la mecánica de heredar y sobreescribir; en la próxima lección, Polimorfismo, verás por qué esta capacidad de sobreescribir métodos es mucho más potente de lo que parece a primera vista, cuando se combina con listas y arrays de la clase base.
- Clases selladas con
sealed
sealedA veces conviene impedir explícitamente que una clase tenga herederas, por ejemplo porque su
diseño no está pensado para extenderse más. Para ello se usa la palabra clave sealed antes de
class:
sealed class Revista : MaterialBibliotecario
{
public int NumeroEdicion { get; set; }
public Revista(string titulo, string autor, int numeroEdicion) : base(titulo, autor)
{
NumeroEdicion = numeroEdicion;
}
public override void MostrarFicha()
{
base.MostrarFicha();
Console.WriteLine($"Numero de edicion: {NumeroEdicion}");
}
}
// class RevistaDigital : Revista { }
// Error de compilacion: no se puede heredar de una clase sellada ('sealed')Marcar Revista como sealed deja claro, para quien lea el código, que no está pensada como
una nueva clase base: es un punto final en esa rama de la jerarquía. MaterialBibliotecario y
Libro, en cambio, se mantienen sin sealed durante el resto del módulo, ya que
MaterialBibliotecario necesita seguir siendo heredable por diseño.
- Jerarquía de una sola herencia en C#
Una limitación importante —y deliberada— de C# es que una clase solo puede heredar de una clase base: no existe la herencia múltiple de clases, a diferencia de otros aspectos del lenguaje (como las interfaces, que sí se pueden combinar varias a la vez, y que verás en el Módulo 4). Esto significa que la jerarquía de herencia de BiblioTech forma siempre un árbol, nunca una red:
classDiagram
MaterialBibliotecario <|-- Libro
MaterialBibliotecario <|-- Revista
class MaterialBibliotecario {
+string Titulo
+string Autor
+bool Disponible
+Prestar()
+Devolver()
+MostrarFicha()
}
class Libro {
+string Isbn
+MostrarFicha()
}
class Revista {
+int NumeroEdicion
+MostrarFicha()
}
Todas las clases en C# —incluida MaterialBibliotecario, aunque no lo hayas escrito
explícitamente— heredan, en última instancia, de una clase raíz común llamada object, de la
que provienen métodos como ToString() que ya has usado indirectamente al interpolar objetos
en cadenas de texto. Esto queda fuera del alcance de esta lección, pero conviene saber que la
jerarquía de clases de C# siempre tiene, en la cima, esa raíz común.
Errores Comunes y Consejos
- Olvidar
base(...)cuando la clase base no tiene constructor sin parámetros: siMaterialBibliotecariosolo defineMaterialBibliotecario(string titulo, string autor), cualquier clase heredera está obligada a indicar: base(...)con esos argumentos; si no, el compilador da un error. - Sobreescribir sin
override(o sinvirtualen la base): si el método de la clase base no esvirtual, la clase heredera no puede usaroverride; y si lo que se busca es ocultar el método con otro nombre distinto sin relación real de sobreescritura (usandonewen la firma del método), el comportamiento resultante suele confundir y rara vez es lo que se pretendía. Usa siemprevirtual/overridecuando la intención sea sobreescribir. - Duplicar código que ya está en la clase base: si
LibroyRevistavuelven a definirTitulo,AutoroDisponiblepor su cuenta, se pierde la ventaja de la herencia y se reintroduce el problema que esta lección resuelve. - Marcar
sealeduna clase que sí necesitas seguir extendiendo:sealedes una decisión de diseño deliberada; aplícalo solo cuando tengas la certeza de que esa clase no debe tener más herederas (comoRevistaen este módulo), no como costumbre. - Esperar herencia múltiple de clases: en C# una clase solo puede heredar de una única clase base; si necesitas combinar varias capacidades independientes, la herramienta adecuada son las interfaces, que verás en el Módulo 4.
Ejercicios
-
Define la clase
MaterialBibliotecariocon las propiedadesTituloyAutor(string) yDisponible(bool, con inicializador de propiedad atrue), un constructorMaterialBibliotecario(string titulo, string autor), y un métodovoid MostrarFicha()que muestre las tres propiedades por consola. -
Crea la clase
Libro : MaterialBibliotecariocon la propiedad adicionalIsbn(string) y un constructorLibro(string titulo, string autor, string isbn)que usebase(...)para inicializar la parte heredada. Crea un objetoLibroy muestra suTitulo(heredado) y suIsbn(propio). -
Crea la clase
Revista : MaterialBibliotecario(sinsealedpor ahora) con la propiedadNumeroEdicion(int) y su propio constructor conbase(...). MarcaMostrarFicha()comovirtualenMaterialBibliotecarioy sobreescríbelo enRevistaconoverride, de forma que, además de la ficha heredada, muestre también el número de edición usandobase.MostrarFicha().
Soluciones
class MaterialBibliotecario
{
public string Titulo { get; set; }
public string Autor { get; set; }
public bool Disponible { get; set; } = true;
public MaterialBibliotecario(string titulo, string autor)
{
Titulo = titulo;
Autor = autor;
}
public void MostrarFicha()
{
Console.WriteLine($"Titulo: {Titulo}");
Console.WriteLine($"Autor: {Autor}");
Console.WriteLine($"Disponible: {Disponible}");
}
}
class Libro : MaterialBibliotecario
{
public string Isbn { get; set; }
public Libro(string titulo, string autor, string isbn) : base(titulo, autor)
{
Isbn = isbn;
}
}
Libro libro1 = new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
Console.WriteLine(libro1.Titulo); // Rayuela
Console.WriteLine(libro1.Isbn); // 978-84-376-0495-4
class MaterialBibliotecario
{
public string Titulo { get; set; }
public string Autor { get; set; }
public bool Disponible { get; set; } = true;
public MaterialBibliotecario(string titulo, string autor)
{
Titulo = titulo;
Autor = autor;
}
public virtual void MostrarFicha()
{
Console.WriteLine($"Titulo: {Titulo}");
Console.WriteLine($"Autor: {Autor}");
Console.WriteLine($"Disponible: {Disponible}");
}
}
class Revista : MaterialBibliotecario
{
public int NumeroEdicion { get; set; }
public Revista(string titulo, string autor, int numeroEdicion) : base(titulo, autor)
{
NumeroEdicion = numeroEdicion;
}
public override void MostrarFicha()
{
base.MostrarFicha();
Console.WriteLine($"Numero de edicion: {NumeroEdicion}");
}
}
Revista revista1 = new Revista("National Geographic", "Varios autores", 302);
revista1.MostrarFicha();
// Titulo: National Geographic
// Autor: Varios autores
// Disponible: True
// Numero de edicion: 302
Conclusión
En esta lección has aprendido a evitar la duplicación de código extrayendo una clase base,
MaterialBibliotecario, de la que heredan Libro y la nueva Revista mediante :; has usado
base para invocar el constructor (y el método) de la clase base, has sobreescrito miembros
heredados con virtual/override, has visto cómo sealed impide que una clase tenga
herederas, y por qué C# solo permite heredar de una única clase base a la vez. El modelo de
dominio de BiblioTech ahora tiene una jerarquía real: MaterialBibliotecario en la base, con
Libro y Revista como especializaciones concretas.
Sin embargo, todavía no has explotado la verdadera potencia de esta jerarquía: poder tratar un
Libro y una Revista de forma uniforme, como si ambos fueran simplemente "un
MaterialBibliotecario", y dejar que cada uno se comporte según su propio tipo en tiempo de
ejecución. Eso es exactamente el polimorfismo, el tema de la próxima lección.
Curso de Programación en C#
Módulo 1: Introducción a C#
- Introducción a C#
- Configuración del Entorno de Desarrollo
- Programa Hola Mundo
- Sintaxis y Estructura Básica
- Variables y Tipos de Datos
- Arrays y Cadenas de Texto
Módulo 2: Estructuras de Control
Módulo 3: Programación Orientada a Objetos
- Clases y Objetos
- Métodos
- Constructores y Destructores
- Herencia
- Polimorfismo
- Encapsulamiento
- Abstracción
- Structs y Records: Tipos por Valor y por Referencia
Módulo 4: Conceptos Avanzados de C#
- Interfaces
- Delegados y Eventos
- Pattern Matching y Características Modernas de C#
- Genéricos
- Colecciones
- LINQ (Consulta Integrada en el Lenguaje)
- Programación Asíncrona
Módulo 5: Trabajando con Datos
- Entrada/Salida de Archivos
- Serialización
- Conectividad con Bases de Datos
- Entity Framework
- Trabajo con JSON y Consumo de APIs REST
Módulo 6: Temas Avanzados
- Reflexión
- Atributos
- Programación Dinámica
- Gestión de Memoria y Recolección de Basura
- Multihilo y Programación Paralela
Módulo 7: Construcción de Aplicaciones
- Formularios de Windows
- WPF (Windows Presentation Foundation)
- ASP.NET Core
- Blazor
- Xamarin y .NET MAUI
Módulo 8: Mejores Prácticas y Patrones de Diseño
- Estándares de Codificación y Mejores Prácticas
- Patrones de Diseño
- Inyección de Dependencias e Inversión de Control
- Pruebas Unitarias
- Revisión y Refactorización de Código
