1. Polymorphism in Action
In programming, polymorphism is like a universal remote: you hit the same "Volume+" button, but it controls your TV, audio system, or AC—each device reacts in its own way, but the interface is the same! Same deal in code: objects of different types can react differently to the same method call, as long as that method is defined as virtual in their common ancestor.
In C#, polymorphism shows up when a variable of a base class (or interface) type can "hold" any of its descendants, and calls to virtual methods on that variable actually run the specific, "real" implementation—the one the object itself defines. This is the backbone of architectures where logic changes on the fly.
Is this useful outside of textbooks?
Heck yeah! Pretty much any project that deals with different but similar objects—zoo animals, UI elements, event handlers, document workflow systems—you name it.
- Lets you build universal algorithms—code that works with abstract stuff, without sweating the details of each implementation.
- Guarantees extensibility—add a hundred new "animals," "shapes," or "handlers" without touching the existing code.
- Cuts down on dependencies between program components (super important for interviews and architecture!).
2. Basic Syntax and How It Works
Let's recall and tweak our Animal, Dog, and Cat classes to show off polymorphism in action. Since we started with a "Virtual Zoo" app, let's keep leveling it up.
Base Classes with Virtual Methods
public class Animal
{
public string Name { get; set; }
public Animal(string name)
{
Name = name;
}
// Virtual method—you can override it
public virtual void MakeSound()
{
Console.WriteLine($"{Name} makes some kind of sound...");
}
}
public class Dog : Animal
{
public Dog(string name) : base(name) { }
public override void MakeSound()
{
Console.WriteLine($"{Name} says: Woof-woof!");
}
}
public class Cat : Animal
{
public Cat(string name) : base(name) { }
public override void MakeSound()
{
Console.WriteLine($"{Name} says: Meow!");
}
}
Using Polymorphism: Example with an Animal Collection
Now let's say we've got a list of animals—some pets, some not—and we want to make all of them "make a sound." Without polymorphism, you'd have to check types and write a bunch of duplicate code. With polymorphism—it's all clean and slick!
// Creating an array of animals of different kinds
Animal[] animals = new Animal[]
{
new Dog("Bobik"),
new Cat("Murka"),
new Dog("Sharik"),
new Cat("Barsik"),
};
// Loop through the array and ask each to make a sound
foreach (var animal in animals)
{
animal.MakeSound(); // Calls Dog or Cat's method, not Animal's!
}
Result:
Bobik says: Woof-woof!
Murka says: Meow!
Sharik says: Woof-woof!
Barsik says: Meow!
That's the magic: one universal piece of code—different results depending on the real object type.
Diagram: How Polymorphism Works
Animal (base class)
/ \
Dog Cat
When you call animal.MakeSound(), where Animal might actually hold a Dog or Cat instance, the .NET runtime figures out which MakeSound() method to call at runtime.
3. Solving Typical Tasks with Polymorphism
Example 1: Universal List, Different Actions
Say you're making a game. You've got a base class GameObject, and subclasses—enemies, friends, obstacles. They can all move, they all have an Update() method, but each one does it differently.
public class GameObject
{
public virtual void Update() { }
}
public class Enemy : GameObject
{
public override void Update()
{
Console.WriteLine("Enemy is advancing!");
}
}
public class Friend : GameObject
{
public override void Update()
{
Console.WriteLine("Friend is helping!");
}
}
GameObject[] objects = new GameObject[]
{
new Enemy(),
new Friend(),
new Enemy()
};
foreach (var obj in objects)
{
obj.Update();
}
// Output:
// Enemy is advancing!
// Friend is helping!
// Enemy is advancing!
Example 2: Passing Objects to Methods
You can accept a base type parameter, but use any subclass. This saves a ton of effort, especially if you'll be adding new descendants later.
public static void FeedAnimal(Animal animal)
{
Console.Write($"{animal.Name}: ");
animal.MakeSound();
Console.WriteLine("And gets some food.");
}
FeedAnimal(new Dog("Rex"));
FeedAnimal(new Cat("Sima"));
// Result:
// Rex: Rex says: Woof-woof!
// And gets some food.
// Sima: Sima says: Meow!
// And gets some food.
Notice: writing this method without polymorphism would be a pain—you'd have to check the animal type, write a bunch of ifs, and call the right methods by hand.
Important Note: Runtime Binding
Polymorphism works because virtual method calls are made dynamically, at runtime. This is called late binding. Even if we know the variable as Animal, the method that gets called is the one defined in the real, "actual" object.
If a method isn't marked virtual, the call always goes to the code declared for the variable's type (not the object's). So don't be shy with the virtual keyword if you want flexibility!
4. Practice: Expanding Our App
Let's say you want to add a new feature to your virtual zoo: now every animal should not only MakeSound(), but also move (Move()). But each one does it differently.
1. Add a Virtual Method to the Base Class
public class Animal
{
public string Name { get; set; }
public Animal(string name)
{
Name = name;
}
public virtual void MakeSound()
{
Console.WriteLine($"{Name} makes some kind of sound...");
}
public virtual void Move()
{
Console.WriteLine($"{Name} moves in an unspecified way...");
}
}
2. Implement Move() Differently in Subclasses
public class Dog : Animal
{
public Dog(string name) : base(name) { }
public override void MakeSound()
{
Console.WriteLine($"{Name} says: Woof-woof!");
}
public override void Move()
{
Console.WriteLine($"{Name} runs after the stick.");
}
}
public class Cat : Animal
{
public Cat(string name) : base(name) { }
public override void MakeSound()
{
Console.WriteLine($"{Name} says: Meow!");
}
public override void Move()
{
Console.WriteLine($"{Name} sneaks on soft paws.");
}
}
3. Use Both Methods in a Collection
Animal[] animals = new Animal[]
{
new Dog("Bim"),
new Cat("Lusya")
};
foreach (var animal in animals)
{
animal.MakeSound();
animal.Move();
}
Result:
Bim says: Woof-woof!
Bim runs after the stick.
Lusya says: Meow!
Lusya sneaks on soft paws.
In real apps, this approach lets you write super powerful and reusable modules. In games, for example, this is the foundation for all object managers—from NPCs to effects.
5. Practical Task: Drawing Different Shapes
Let's check out an example that's not about zoos, but graphics. We'll make a base class Shape with a virtual Draw() method, and then extend it.
public class Shape
{
public virtual void Draw()
{
Console.WriteLine("Drawing an undefined shape.");
}
}
public class Circle : Shape
{
public override void Draw()
{
Console.WriteLine("Drawing a circle.");
}
}
public class Rectangle : Shape
{
public override void Draw()
{
Console.WriteLine("Drawing a rectangle.");
}
}
// Collection of shapes
Shape[] shapes = new Shape[]
{
new Circle(),
new Rectangle(),
new Circle()
};
foreach (var shape in shapes)
{
shape.Draw();
}
Here, Draw() is called on a Shape variable, but the actual methods from Circle or Rectangle get called. In real graphics editors and libraries, like WinForms or WPF, that's exactly how it works.
6. General Approach: Writing Universal Algorithms
Polymorphism turns your code into a flexible and extensible tool. For example, your collection can have not just Dog and Cat, but Hamster, Parrot, whatever—and you don't write a single new line to handle them all in the same way.
Plus, you can easily pass descendant objects to methods that take the base type:
void PrintAnimalInfo(Animal animal)
{
Console.WriteLine($"Name: {animal.Name}");
animal.MakeSound();
animal.Move();
}
Animal hamster = new Animal("Homa");
Animal dog = new Dog("Lord");
PrintAnimalInfo(hamster); // Uses Animal's method
PrintAnimalInfo(dog); // Uses Dog's version
7. Handy Nuances
Common Questions and Gotchas
Lots of newbie devs think that if you make a Dog object and then declare a variable as Dog myDog, it's the same as Animal myDog. Actually, if you declare it as Animal, it "sees" only what's in Animal (except for overridden methods), but if you declare it as Dog, it sees everything: Bark(), custom properties, etc.
Also, keep in mind: if a method in the base class isn't marked virtual, you can't override it. If you try to write override for such a method in a subclass, the compiler will throw a fit and give you an error.
By the way, if you really want to replace a non-virtual method, use the new keyword, but that's for advanced (and sometimes messy!) cases.
Why do interviewers love to ask about this?
Because polymorphism is like a Swiss Army knife for devs: if you get how to use it, you can build extensible and maintainable systems, and your code will not just work, but live long and prosper. For example, they might ask you to handle different payment forms (classic: BankCard, PayPal, Bitcoin)—and expect you to make a common interface (or abstract base class) with a Pay() method, so clients can call Pay(BankCard), Pay(PayPal), Pay(Bitcoin)—without caring how it works inside.
8. Typical Beginner Mistakes
One of the most common mistakes is trying to call a subclass-specific method through a base type variable. Like this:
Animal animal = new Dog("Tuzik");
animal.Bark(); // Error! Animal doesn't have a Bark method.
Why doesn't this work? Because a variable of type Animal "sees" only what's declared in Animal, even if it's actually a Dog. You can only call what's defined in the base class—and overridden (override) in descendants.
If you really need to call a method that's only in Dog, you'll have to cast:
Animal animal = new Dog("Tuzik");
if (animal is Dog dog)
{
dog.Bark();
}
But honestly, if you're doing this a lot, your architecture might be off (or you're overusing inheritance).
GO TO FULL VERSION