En la lección anterior conseguiste que Libro y Revista compartieran, a través de
MaterialBibliotecario, las propiedades y métodos comunes a cualquier material de la
biblioteca. Pero por ahora los sigues tratando por separado: si quisieras mostrar la
descripción de todos los materiales del catálogo, necesitarías dos bucles distintos —uno para
libros y otro para revistas— o una serie de comprobaciones manuales de tipo. El
polimorfismo (del griego "muchas formas") es la capacidad de tratar objetos de distintas
clases heredadas de una misma base de manera uniforme, dejando que cada uno se comporte según
su propio tipo real en tiempo de ejecución. En esta lección aprenderás a recorrer todo el
catálogo de BiblioTech —libros y revistas mezclados— con un único bucle, y cada material
"sabrá" describirse a sí mismo correctamente.
Contenido
- Dos tipos de polimorfismo: recordatorio del ya visto
- Polimorfismo en tiempo de ejecución: el método
Describir() - Enlace dinámico: quién decide qué versión se ejecuta
- Un catálogo mixto:
MaterialBibliotecario[]recorrido polimórficamente - El operador
is: comprobar el tipo real de un objeto - El operador
as: castear de forma seleccionada compilando siempre - Aplicación: filtrar el catálogo por tipo concreto
- Dos tipos de polimorfismo: recordatorio del ya visto
En C# se habla de dos formas de polimorfismo:
| Tipo de polimorfismo | Cuándo se decide qué código se ejecuta | Dónde lo viste |
|---|---|---|
| En tiempo de compilación (estático) | El compilador, según el número/tipo de argumentos | Sobrecarga de métodos, Lección "Métodos" |
| En tiempo de ejecución (dinámico) | El programa, al ejecutarse, según el tipo real del objeto | virtual/override, tema de esta lección |
Ya usaste el primero sin llamarlo por su nombre: cuando definiste MostrarFicha() y
MostrarFicha(bool incluirIsbn) en la lección de Métodos, el compilador decidía, en el momento
de compilar el programa, cuál de las dos versiones invocar, según cuántos argumentos le
pasaras. Esta lección se centra en el segundo tipo, mucho más característico de la
programación orientada a objetos.
- Polimorfismo en tiempo de ejecución: el método
Describir()
Describir()En la lección anterior, MostrarFicha() ya era virtual en MaterialBibliotecario y se
sobreescribía en Libro y Revista. Vamos a añadir un segundo método virtual, Describir(),
pensado específicamente para ilustrar el polimorfismo en acción: en lugar de imprimir
directamente por consola, devuelve un string con una descripción breve del material,
distinta según su tipo concreto.
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 string Describir()
{
return $"Material: {Titulo}, de {Autor}";
}
}
class Libro : MaterialBibliotecario
{
public string Isbn { get; set; }
public Libro(string titulo, string autor, string isbn) : base(titulo, autor)
{
Isbn = isbn;
}
public override string Describir()
{
return $"Libro: {Titulo}, de {Autor} (ISBN {Isbn})";
}
}
class Revista : MaterialBibliotecario
{
public int NumeroEdicion { get; set; }
public Revista(string titulo, string autor, int numeroEdicion) : base(titulo, autor)
{
NumeroEdicion = numeroEdicion;
}
public override string Describir()
{
return $"Revista: {Titulo}, edicion numero {NumeroEdicion}";
}
}Cada clase ofrece su propia versión de Describir(), adaptada a lo que tiene sentido mostrar
para ese tipo concreto de material.
- Enlace dinámico: quién decide qué versión se ejecuta
Ahora viene la parte clave del polimorfismo. Observa este código:
MaterialBibliotecario material = new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
Console.WriteLine(material.Describir());
// Libro: Rayuela, de Julio Cortazar (ISBN 978-84-376-0495-4)La variable material está declarada como tipo MaterialBibliotecario, pero el objeto al que
apunta es, en realidad, un Libro. Al llamar a material.Describir(), C# no ejecuta la
versión de MaterialBibliotecario (que sería lo esperable si solo se mirara el tipo de la
variable), sino la versión sobreescrita en Libro, porque ese es el tipo real del objeto en
memoria. A este mecanismo se le llama enlace dinámico (dynamic binding o late
binding): qué código se ejecuta se decide en tiempo de ejecución, según el tipo real del
objeto, no según el tipo declarado de la variable que lo referencia.
Esto solo ocurre porque Describir() es virtual en la clase base y está override en la
heredera; si Describir() no fuera virtual, material.Describir() ejecutaría siempre la
versión de MaterialBibliotecario, sin importar qué objeto concreto contuviera realmente la
variable.
- Un catálogo mixto:
MaterialBibliotecario[] recorrido polimórficamente
MaterialBibliotecario[] recorrido polimórficamenteAquí es donde el polimorfismo demuestra su verdadera utilidad: un array (o cualquier colección) declarado con el tipo base puede contener objetos de cualquiera de sus clases herederas, mezclados, y recorrerse con un único bucle:
MaterialBibliotecario[] catalogo = new MaterialBibliotecario[]
{
new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4"),
new Revista("National Geographic", "Varios autores", 302),
new Libro("Ficciones", "Jorge Luis Borges", "978-84-376-0496-1")
};
foreach (MaterialBibliotecario material in catalogo)
{
Console.WriteLine(material.Describir());
}Salida:
Libro: Rayuela, de Julio Cortazar (ISBN 978-84-376-0495-4) Revista: National Geographic, edicion numero 302 Libro: Ficciones, de Jorge Luis Borges (ISBN 978-84-376-0496-1)
Un único bucle foreach, escrito una sola vez sobre el tipo base MaterialBibliotecario,
produce automáticamente la descripción correcta para cada elemento, sea Libro o Revista,
sin que el bucle necesite saber nada sobre esos tipos concretos ni contener ninguna
comprobación explícita de tipo. Añadir mañana una tercera clase heredera (por ejemplo, un
futuro Dvd : MaterialBibliotecario) no requeriría tocar este bucle en absoluto: bastaría con
que la nueva clase sobreescribiera Describir() a su manera. Esta es la esencia del
polimorfismo: el código que recorre el catálogo permanece estable aunque el catálogo crezca en
tipos distintos.
- El operador
is: comprobar el tipo real de un objeto
is: comprobar el tipo real de un objetoA veces, sin embargo, necesitas hacer algo específico de un tipo concreto, no solo lo que
ofrece la clase base. El operador is comprueba si un objeto es (o deriva de) un tipo
determinado, y opcionalmente lo asigna directamente a una variable de ese tipo más específico:
foreach (MaterialBibliotecario material in catalogo)
{
if (material is Libro libro)
{
Console.WriteLine($"Es un libro; su ISBN es {libro.Isbn}");
}
else if (material is Revista revista)
{
Console.WriteLine($"Es una revista; su numero de edicion es {revista.NumeroEdicion}");
}
}material is Libro libro se lee como "si material es (en tiempo de ejecución) un Libro,
entonces asigna esa misma referencia, ya con el tipo Libro, a una nueva variable llamada
libro". Dentro del bloque if, libro ya da acceso a Isbn, propiedad que no existe en
MaterialBibliotecario y que, por tanto, no sería accesible directamente a través de la
variable material. Este patrón se llama pattern matching con is, y se ampliará bastante
en el Módulo 4 ("Pattern Matching y Características Modernas").
- El operador
as: castear de forma segura
as: castear de forma seguraEl operador as intenta convertir (castear) un objeto a un tipo más específico, y devuelve
null si la conversión no es posible, en lugar de lanzar una excepción:
MaterialBibliotecario primerElemento = catalogo[0];
Libro? libroCasteado = primerElemento as Libro;
if (libroCasteado != null)
{
Console.WriteLine($"Casteo correcto: {libroCasteado.Isbn}");
}
else
{
Console.WriteLine("El elemento no era un Libro.");
}Si primerElemento fuera en realidad una Revista, primerElemento as Libro no lanzaría
ningún error: simplemente libroCasteado valdría null, y el código puede comprobarlo con
tranquilidad. Esto contrasta con un casteo directo entre paréntesis, como (Libro) primerElemento, que si el objeto no es realmente un Libro, sí lanza una excepción
(InvalidCastException) en tiempo de ejecución. En la práctica moderna de C#, is con
patrón (apartado anterior) suele preferirse a as para este tipo de comprobaciones, por ser
más compacto y legible, pero conviene conocer ambos.
| Operador/técnica | Si el tipo no coincide | Cuándo usarlo |
|---|---|---|
(Libro) material (casteo directo) |
Lanza InvalidCastException |
Cuando estás seguro del tipo y un error sería una señal real de fallo |
material as Libro |
Devuelve null |
Cuando la conversión puede fallar razonablemente y prefieres comprobar null |
material is Libro libro |
La condición del if es false (no entra al bloque) |
La forma más legible en C# moderno; combina comprobación y asignación en un paso |
- Aplicación: filtrar el catálogo por tipo concreto
Cerramos la lección combinando el recorrido polimórfico con is, para contar cuántos
elementos del catálogo son libros y cuántos son revistas —una operación habitual al generar
estadísticas del catálogo de BiblioTech:
MaterialBibliotecario[] catalogo = new MaterialBibliotecario[]
{
new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4"),
new Revista("National Geographic", "Varios autores", 302),
new Libro("Ficciones", "Jorge Luis Borges", "978-84-376-0496-1"),
new Revista("Muy Interesante", "Varios autores", 487)
};
int totalLibros = 0;
int totalRevistas = 0;
foreach (MaterialBibliotecario material in catalogo)
{
Console.WriteLine(material.Describir()); // polimorfismo: cada uno se describe a su manera
if (material is Libro)
{
totalLibros++;
}
else if (material is Revista)
{
totalRevistas++;
}
}
Console.WriteLine($"Total de libros: {totalLibros}");
Console.WriteLine($"Total de revistas: {totalRevistas}");Nota que en if (material is Libro), sin declarar una variable adicional, basta para
preguntar "¿es este objeto un Libro?" sin necesidad de acceder a ninguna de sus propiedades
específicas —útil cuando solo interesa contar o clasificar, no operar con los datos propios del
tipo concreto.
Errores Comunes y Consejos
- Olvidar
virtualen la clase base: siDescribir()no esvirtual, escribiroverrideenLibrooRevistaprovoca un error de compilación; y si en su lugar usas la palabra clavenewpara "ocultar" el método (en vez de sobreescribirlo), perderás el polimorfismo:material.Describir()volvería a ejecutar siempre la versión deMaterialBibliotecario, aunque el objeto real fuera unLibro. - Castear directamente sin comprobar el tipo:
(Libro) materialsin verificar antes conispuede lanzarInvalidCastExceptionsi el objeto real no es unLibro; prefiereiscon patrón oascuando no estés seguro del tipo real. - Escribir un bucle distinto para cada tipo heredero: si te sorprendes escribiendo un
foreachpara libros y otro casi idéntico para revistas, es una señal de que deberías estar recorriendo el tipo base (MaterialBibliotecario[]) con un único bucle polimórfico, como en el apartado 4. - Confundir el tipo de la variable con el tipo real del objeto: una variable declarada como
MaterialBibliotecariopuede apuntar a unLibroo a unaRevista; lo que determina qué versión de un métodovirtualse ejecuta es siempre el tipo real del objeto, nunca el tipo con el que se declaró la variable. - Consejo: antes de recurrir a
is/aspara distinguir tipos dentro de un bucle, pregúntate si ese comportamiento distinto no podría resolverse simplemente sobreescribiendo un métodovirtualen cada clase heredera; suele ser un diseño más limpio y extensible.
Ejercicios
-
Sobre las clases
MaterialBibliotecario,LibroyRevistadefinidas en esta lección (conDescribir()como métodovirtual/override), crea un arrayMaterialBibliotecario[]con dos libros y una revista, y recórrelo conforeachmostrando el resultado deDescribir()de cada uno. -
Sobre el mismo array del ejercicio anterior, usa el operador
iscon patrón para mostrar, solo para los elementos que seanLibro, un mensaje con su ISBN (por ejemplo:"ISBN: 978-84-376-0495-4"). -
Escribe una expresión con el operador
asque intente castear el primer elemento del array aRevista, y muestre"Es una revista con edicion <numero>"si el casteo tiene éxito, o"No es una revista"si el resultado esnull.
Soluciones
MaterialBibliotecario[] catalogo = new MaterialBibliotecario[]
{
new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4"),
new Libro("Ficciones", "Jorge Luis Borges", "978-84-376-0496-1"),
new Revista("National Geographic", "Varios autores", 302)
};
foreach (MaterialBibliotecario material in catalogo)
{
Console.WriteLine(material.Describir());
}
foreach (MaterialBibliotecario material in catalogo)
{
if (material is Libro libro)
{
Console.WriteLine($"ISBN: {libro.Isbn}");
}
}
Revista? revistaCasteada = catalogo[0] as Revista;
if (revistaCasteada != null)
{
Console.WriteLine($"Es una revista con edicion {revistaCasteada.NumeroEdicion}");
}
else
{
Console.WriteLine("No es una revista");
}
Como catalogo[0] es en realidad un Libro (según el ejercicio 1), el casteo con as
devuelve null, y el resultado mostrado es "No es una revista".
Conclusión
En esta lección has visto el polimorfismo en tiempo de ejecución en acción: un método
virtual como Describir(), sobreescrito de forma distinta en Libro y Revista, permite
recorrer un catálogo mixto (MaterialBibliotecario[]) con un único bucle y obtener el
comportamiento correcto para cada objeto según su tipo real —gracias al enlace dinámico. También
has aprendido a usar is y as para acceder puntualmente a las particularidades de un tipo
concreto cuando de verdad lo necesitas, sin renunciar al polimorfismo como estrategia principal.
Hasta ahora, sin embargo, Disponible se puede modificar libremente desde fuera de la clase
(material.Disponible = true;, sin pasar por Prestar() ni Devolver()), lo cual podría dejar
el catálogo de BiblioTech en un estado incoherente por error o descuido. En la próxima lección,
Encapsulamiento, aprenderás a proteger el estado interno de tus clases, permitiendo que solo
se modifique a través de los métodos pensados para ello.
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
