CodeGym /Cursos /C# SELF /Uso del polimorfismo en la práctica

Uso del polimorfismo en la práctica

C# SELF
Nivel 21 , Lección 3
Disponible

1. Polimorfismo en la práctica

En programación, polimorfismo es como un mando a distancia universal: pulsas un botón "Volumen+", y controla la tele, el equipo de música o el aire acondicionado — cada aparato reacciona a su manera, ¡pero la interfaz es la misma! Igual, los objetos de diferentes tipos pueden reaccionar de forma distinta a la misma llamada de método, si ese método está definido en su ancestro común como virtual.

En C#, el polimorfismo aparece cuando una variable del tipo de la clase base (o interfaz) puede "contener" un objeto de cualquiera de sus descendientes, y las llamadas a métodos virtuales en esa variable ejecutan la implementación específica, la "real", la que definió el propio objeto. Esto es la base de arquitecturas donde la lógica cambia dinámicamente.

¿Esto sirve para algo fuera de los libros?

¡Sí! Prácticamente en cualquier proyecto donde trabajes con objetos diferentes pero parecidos — desde animales en un zoo hasta elementos de interfaz gráfica, desde manejadores de eventos hasta sistemas de gestión documental.

  • Permite crear algoritmos universales — código que trabaja con entidades abstractas, sin preocuparse por los detalles de la implementación concreta.
  • Garantiza la extensibilidad — puedes añadir cientos de nuevos tipos de "animales", "figuras", "handlers", sin tocar el código existente.
  • Reduce la dependencia entre componentes del programa (¡esto es clave para entrevistas y arquitectura!).

2. Sintaxis básica y mecánica

Vamos a recordar y mejorar nuestras clases Animal, Dog, Cat, para demostrar el polimorfismo en acción. Ya que empezamos con la app "Zoo Virtual", vamos a mejorarla.

Clases base con métodos virtuales


public class Animal
{
    public string Name { get; set; }

    public Animal(string name)
    {
        Name = name;
    }

    // Método virtual — se puede sobrescribir
    public virtual void MakeSound()
    {
        Console.WriteLine($"{Name} emite algún sonido...");
    }
}
        
public class Dog : Animal
{
    public Dog(string name) : base(name) { }

    public override void MakeSound()
    {
        Console.WriteLine($"{Name} dice: ¡Guau-guau!");
    }
}
public class Cat : Animal
{
    public Cat(string name) : base(name) { }

    public override void MakeSound()
    {
        Console.WriteLine($"{Name} dice: ¡Miau!");
    }
}

Usando polimorfismo: ejemplo con una colección de animales

Ahora imagina que tienes una lista de animales — domésticos y no tanto — y quieres que todos "hagan" un sonido. Sin polimorfismo tendrías que comprobar el tipo y escribir mucho código repetido. ¡Con polimorfismo es elegante y compacto!


// Creamos un array de animales de diferentes tipos
Animal[] animals = new Animal[]
{
    new Dog("Bobik"),
    new Cat("Murka"),
    new Dog("Sharik"),
    new Cat("Barsik"),
};

// Recorremos todo el array y pedimos a cada uno que haga un sonido
foreach (var animal in animals)
{
    animal.MakeSound(); // ¡Se llamará al método de Dog o Cat, no de Animal!
}
        

Resultado:

Bobik dice: ¡Guau-guau!
Murka dice: ¡Miau!
Sharik dice: ¡Guau-guau!
Barsik dice: ¡Miau!

Esa es toda la magia: un solo código universal — diferentes resultados según el tipo real del objeto.

Diagrama: cómo funciona el polimorfismo


Animal (clase base)
   /           \
Dog           Cat
        
Jerarquía de clases: Animal → Dog, Cat

Cuando llamas a animal.MakeSound(), donde Animal puede contener una instancia de Dog o Cat, el entorno .NET en tiempo de ejecución decide por sí mismo qué método MakeSound() debe llamar.

3. Resolviendo tareas típicas con polimorfismo

Ejemplo 1: Lista universal, acciones diferentes

Imagina que estás haciendo un juego. Tienes una clase base GameObject, y derivadas — enemigos, aliados, obstáculos. Todos pueden moverse, todos tienen el método Update(), pero la implementación es diferente.

public class GameObject
{
    public virtual void Update() { }
}

public class Enemy : GameObject
{
    public override void Update()
    {
        Console.WriteLine("¡El enemigo avanza!");
    }
}

public class Friend : GameObject
{
    public override void Update()
    {
        Console.WriteLine("¡El aliado ayuda!");
    }
}

GameObject[] objects = new GameObject[]
{
    new Enemy(),
    new Friend(),
    new Enemy()
};

foreach (var obj in objects)
{
    obj.Update();
}
// Mostrará:
// ¡El enemigo avanza!
// ¡El aliado ayuda!
// ¡El enemigo avanza!

Ejemplo 2: Pasando objetos a métodos

Puedes aceptar un parámetro del tipo base, y usar cualquier tipo derivado. Esto ahorra mucho trabajo, sobre todo si el objeto se va a ampliar con nuevos descendientes.

public static void FeedAnimal(Animal animal)
{
    Console.Write($"{animal.Name}: ");
    animal.MakeSound();
    Console.WriteLine("Y recibe comida.");
}

FeedAnimal(new Dog("Rex"));
FeedAnimal(new Cat("Sima"));

// Resultado:
// Rex: Rex dice: ¡Guau-guau!
// Y recibe comida.
// Sima: Sima dice: ¡Miau!
// Y recibe comida.

Fíjate: escribir un método así sin polimorfismo sería muy costoso — tendrías que comprobar el tipo de animal, hacer muchos ifs y llamar a los métodos correctos a mano.

Nota importante: enlace en tiempo de ejecución

El polimorfismo funciona porque las llamadas a métodos virtuales se hacen de forma dinámica, es decir, en tiempo de ejecución. Esto se llama late binding. Aunque conozcamos la variable como Animal, el método que se llama es el que está definido en el objeto real.

Si el método no está marcado como virtual, la llamada siempre irá al código declarado para la variable (no para el objeto). Así que no dudes en usar la palabra clave virtual si quieres flexibilidad.

4. Práctica: ampliando nuestra app

Imagina que decides añadir una nueva feature a tu zoo virtual: ahora cada animal debe hacer no solo MakeSound(), sino también moverse (Move()). Pero cada uno lo hace a su manera.

1. Añadimos un método virtual en la clase base


public class Animal
{
    public string Name { get; set; }

    public Animal(string name)
    {
        Name = name;
    }

    public virtual void MakeSound()
    {
        Console.WriteLine($"{Name} emite algún sonido...");
    }

    public virtual void Move()
    {
        Console.WriteLine($"{Name} se mueve de forma no especificada...");
    }
}
        

2. En las clases derivadas implementamos Move() a nuestra manera

public class Dog : Animal
{
    public Dog(string name) : base(name) { }

    public override void MakeSound()
    {
        Console.WriteLine($"{Name} dice: ¡Guau-guau!");
    }

    public override void Move()
    {
        Console.WriteLine($"{Name} corre tras el palo.");
    }
}

public class Cat : Animal
{
    public Cat(string name) : base(name) { }

    public override void MakeSound()
    {
        Console.WriteLine($"{Name} dice: ¡Miau!");
    }

    public override void Move()
    {
        Console.WriteLine($"{Name} se desliza con patas suaves.");
    }
}

3. Usamos ambos métodos en la colección

Animal[] animals = new Animal[]
{
    new Dog("Bim"),
    new Cat("Lusia")
};

foreach (var animal in animals)
{
    animal.MakeSound();
    animal.Move();
}

Resultado:

Bim dice: ¡Guau-guau!
Bim corre tras el palo.
Lusia dice: ¡Miau!
Lusia se desliza con patas suaves.

En apps reales, este enfoque permite escribir módulos muy potentes y reutilizables. Por ejemplo, en juegos esto es la base de todos los managers de objetos — desde NPCs hasta efectos.

5. Ejercicio práctico: dibujando diferentes figuras

Veamos un ejemplo fuera del zoo, en gráficos. Creamos una clase base Shape con un método virtual Draw(), y luego la ampliamos.

public class Shape
{
    public virtual void Draw()
    {
        Console.WriteLine("Se dibuja una figura indefinida.");
    }
}

public class Circle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Se dibuja un círculo.");
    }
}

public class Rectangle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Se dibuja un rectángulo.");
    }
}

// Colección de figuras
Shape[] shapes = new Shape[]
{
    new Circle(),
    new Rectangle(),
    new Circle()
};

foreach (var shape in shapes)
{
    shape.Draw();
}

Aquí Draw() se llama sobre una variable de tipo Shape, pero en realidad se ejecutan los métodos de Circle o Rectangle. En editores gráficos y librerías reales, como WinForms o WPF — así es como funciona todo.

6. Enfoque general: escribiendo algoritmos universales

El polimorfismo convierte tu código en una herramienta flexible y ampliable. Por ejemplo, en la colección pueden estar no solo Dog y Cat, sino también Hamster, Parrot y quien sea — y no escribes ni una línea nueva para tratarlos de forma común.

Además, puedes pasar objetos descendientes a métodos que aceptan el tipo base sin problema:

void PrintAnimalInfo(Animal animal)
{
    Console.WriteLine($"Nombre: {animal.Name}");
    animal.MakeSound();
    animal.Move();
}

Animal hamster = new Animal("Homa");
Animal dog = new Dog("Lord");
PrintAnimalInfo(hamster); // Usa el método de Animal
PrintAnimalInfo(dog);     // Usa la versión de Dog

7. Detalles útiles

Preguntas frecuentes y trampas típicas

Muchos programadores novatos piensan que si crean un objeto Dog y luego declaran la variable Dog myDog, no hay diferencia con Animal myDog. En realidad, si declaras la variable como Animal, solo "ve" lo que hay en Animal (excepto los métodos sobrescritos), y si la declaras como Dog — ve todo: Bark() y propiedades específicas.

También es importante saber: si el método en la clase base no está marcado como virtual, no se puede sobrescribir. Si intentas poner override en el hijo para ese método — el compilador se quejará y dará error.

Por cierto, si realmente quieres reemplazar un método que no es virtual, usa la palabra clave new, pero eso ya es tema para casos avanzados (¡y conflictivos!).

¿Por qué esto es tan popular en entrevistas?

Porque el polimorfismo es como una navaja suiza para el programador: si sabes cómo usarlo, puedes construir sistemas ampliables y mantenibles, y tu código no solo funcionará, sino que vivirá feliz mucho tiempo. Por ejemplo, te pueden pedir implementar el manejo de diferentes formas de pago (lo típico: BankCard, PayPal, Bitcoin) — y esperan que hagas una interfaz común (o clase base abstracta) con el método Pay(), y luego los clientes podrán aceptar Pay(BankCard), Pay(PayPal), Pay(Bitcoin) — sin saber cómo funciona por dentro.

8. Errores típicos de principiantes

Uno de los errores más comunes es intentar llamar a un método específico de la clase derivada usando una variable del tipo base. Por ejemplo:


Animal animal = new Dog("Tuzik");
animal.Bark(); // ¡Error! Animal no tiene el método Bark.
        

¿Por qué no funciona? Porque una variable de tipo Animal solo "ve" lo que está declarado en Animal, aunque en realidad sea un Dog. Solo puedes llamar a lo que está definido en la clase base — y sobrescrito (override) en los hijos.

Si realmente necesitas llamar a un método que solo existe en Dog — tendrás que hacer un cast:

Animal animal = new Dog("Tuzik");
if (animal is Dog dog)
{
    dog.Bark();
}

Pero muchas veces, si haces esto, es que algo falla en la arquitectura (o estás abusando de la herencia).

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION