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

  1. Dos tipos de polimorfismo: recordatorio del ya visto
  2. Polimorfismo en tiempo de ejecución: el método Describir()
  3. Enlace dinámico: quién decide qué versión se ejecuta
  4. Un catálogo mixto: MaterialBibliotecario[] recorrido polimórficamente
  5. El operador is: comprobar el tipo real de un objeto
  6. El operador as: castear de forma seleccionada compilando siempre
  7. Aplicación: filtrar el catálogo por tipo concreto

  1. 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.

  1. Polimorfismo en tiempo de ejecución: el método 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.

  1. 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.

  1. Un catálogo mixto: MaterialBibliotecario[] recorrido polimórficamente

Aquí 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.

  1. El operador is: comprobar el tipo real de un objeto

A 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").

  1. El operador as: castear de forma segura

El 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

  1. 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 virtual en la clase base: si Describir() no es virtual, escribir override en Libro o Revista provoca un error de compilación; y si en su lugar usas la palabra clave new para "ocultar" el método (en vez de sobreescribirlo), perderás el polimorfismo: material.Describir() volvería a ejecutar siempre la versión de MaterialBibliotecario, aunque el objeto real fuera un Libro.
  • Castear directamente sin comprobar el tipo: (Libro) material sin verificar antes con is puede lanzar InvalidCastException si el objeto real no es un Libro; prefiere is con patrón o as cuando no estés seguro del tipo real.
  • Escribir un bucle distinto para cada tipo heredero: si te sorprendes escribiendo un foreach para 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 MaterialBibliotecario puede apuntar a un Libro o a una Revista; lo que determina qué versión de un método virtual se ejecuta es siempre el tipo real del objeto, nunca el tipo con el que se declaró la variable.
  • Consejo: antes de recurrir a is/as para distinguir tipos dentro de un bucle, pregúntate si ese comportamiento distinto no podría resolverse simplemente sobreescribiendo un método virtual en cada clase heredera; suele ser un diseño más limpio y extensible.

Ejercicios

  1. Sobre las clases MaterialBibliotecario, Libro y Revista definidas en esta lección (con Describir() como método virtual/override), crea un array MaterialBibliotecario[] con dos libros y una revista, y recórrelo con foreach mostrando el resultado de Describir() de cada uno.

  2. Sobre el mismo array del ejercicio anterior, usa el operador is con patrón para mostrar, solo para los elementos que sean Libro, un mensaje con su ISBN (por ejemplo: "ISBN: 978-84-376-0495-4").

  3. Escribe una expresión con el operador as que intente castear el primer elemento del array a Revista, y muestre "Es una revista con edicion <numero>" si el casteo tiene éxito, o "No es una revista" si el resultado es null.

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#

Módulo 2: Estructuras de Control

Módulo 3: Programación Orientada a Objetos

Módulo 4: Conceptos Avanzados de C#

Módulo 5: Trabajando con Datos

Módulo 6: Temas Avanzados

Módulo 7: Construcción de Aplicaciones

Módulo 8: Mejores Prácticas y Patrones de Diseño

Módulo 9: Proyecto Final

© Copyright 2026. Todos los derechos reservados