C# tutorials > Core C# Fundamentals > Object-Oriented Programming (OOP) > What is polymorphism in C#, and how is it achieved (inheritance, interfaces, generics)?
What is polymorphism in C#, and how is it achieved (inheritance, interfaces, generics)?
Polymorphism, one of the core principles of Object-Oriented Programming (OOP), allows objects of different classes to be treated as objects of a common type. In C#, polymorphism enables you to write code that can work with objects of different types in a uniform manner, making your code more flexible, maintainable, and extensible. This tutorial will delve into how polymorphism is achieved in C# through inheritance, interfaces, and generics.
Understanding Polymorphism
Polymorphism literally means 'many forms.' In the context of OOP, it refers to the ability of an object to take on many forms. This allows you to write code that operates on a base class or interface, without needing to know the specific type of the object at compile time. This leads to more flexible and reusable code. There are two main types of polymorphism: compile-time (static) and runtime (dynamic) polymorphism.
Polymorphism through Inheritance (Runtime Polymorphism)
Inheritance is a powerful mechanism for achieving polymorphism. By deriving classes from a base class, you can override virtual methods in the base class to provide specific implementations for each derived class. The virtual
keyword in the base class allows derived classes to override the method using the override
keyword. At runtime, the correct version of the method is called based on the actual type of the object, even when accessed through a base class reference.
public class Animal
{
public virtual string MakeSound()
{
return "Generic animal sound";
}
}
public class Dog : Animal
{
public override string MakeSound()
{
return "Woof!";
}
}
public class Cat : Animal
{
public override string MakeSound()
{
return "Meow!";
}
}
public class Program
{
public static void Main(string[] args)
{
Animal animal1 = new Animal();
Animal animal2 = new Dog();
Animal animal3 = new Cat();
Console.WriteLine(animal1.MakeSound()); // Output: Generic animal sound
Console.WriteLine(animal2.MakeSound()); // Output: Woof!
Console.WriteLine(animal3.MakeSound()); // Output: Meow!
}
}
Concepts Behind the Snippet (Inheritance)
The code demonstrates inheritance polymorphism. The Animal
class defines a virtual method MakeSound()
. The Dog
and Cat
classes inherit from Animal
and override the MakeSound()
method to provide their specific sounds. When you create instances of Dog
and Cat
and assign them to Animal
type variables, the correct MakeSound()
method is called at runtime, based on the actual type of the object.
Polymorphism through Interfaces
Interfaces provide another way to achieve polymorphism. An interface defines a contract that classes can implement. Any class that implements an interface must provide an implementation for all the members defined in the interface. This allows you to treat objects of different classes that implement the same interface in a uniform way.
public interface ISpeakable
{
string Speak();
}
public class Bird : ISpeakable
{
public string Speak()
{
return "Chirp!";
}
}
public class Person : ISpeakable
{
public string Speak()
{
return "Hello!";
}
}
public class Program
{
public static void Main(string[] args)
{
ISpeakable speaker1 = new Bird();
ISpeakable speaker2 = new Person();
Console.WriteLine(speaker1.Speak()); // Output: Chirp!
Console.WriteLine(speaker2.Speak()); // Output: Hello!
}
}
Concepts Behind the Snippet (Interfaces)
The ISpeakable
interface defines a Speak()
method. The Bird
and Person
classes implement the ISpeakable
interface and provide their own implementations of the Speak()
method. This allows you to treat Bird
and Person
objects as ISpeakable
objects and call the Speak()
method without knowing their specific types.
Polymorphism through Generics (Compile-time Polymorphism)
Generics allow you to write code that can work with different types without having to write separate code for each type. This is achieved by using type parameters, which are placeholders for the actual types that will be used when the code is instantiated. Generics provide compile-time type safety, ensuring that the correct types are used throughout the code.
public class DataStore<T>
{
private T _data;
public DataStore(T initialData)
{
_data = initialData;
}
public T GetData()
{
return _data;
}
public void SetData(T newData)
{
_data = newData;
}
}
public class Program
{
public static void Main(string[] args)
{
DataStore<int> intDataStore = new DataStore<int>(10);
DataStore<string> stringDataStore = new DataStore<string>("Hello");
Console.WriteLine(intDataStore.GetData()); // Output: 10
Console.WriteLine(stringDataStore.GetData()); // Output: Hello
}
}
Concepts Behind the Snippet (Generics)
The DataStore
class is a generic class that can store data of any type. The T
is a type parameter that represents the type of data that will be stored. When you create an instance of DataStore
, you specify the actual type that will be used. In the example, we create a DataStore
to store integers and a DataStore
to store strings. This allows you to reuse the same DataStore
class for different types of data.
Real-Life Use Case: Drawing Shapes
Consider a drawing application. You might have different shapes like circles, rectangles, and triangles. You can define a base class Shape
with abstract methods like Area()
and Draw()
. Each derived class (Circle
, Rectangle
) would then provide its own implementation for these methods. You can then store these shapes in a list of Shape
objects and iterate through them, calling the Draw()
and Area()
methods without needing to know the specific type of each shape. This demonstrates the power of polymorphism in handling different types of objects in a uniform way.
public abstract class Shape
{
public abstract double Area();
public abstract void Draw();
}
public class Circle : Shape
{
public double Radius { get; set; }
public override double Area()
{
return Math.PI * Radius * Radius;
}
public override void Draw()
{
Console.WriteLine("Drawing a circle.");
}
}
public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public override double Area()
{
return Width * Height;
}
public override void Draw()
{
Console.WriteLine("Drawing a rectangle.");
}
}
public class Program
{
public static void Main(string[] args)
{
List<Shape> shapes = new List<Shape>
{
new Circle { Radius = 5 },
new Rectangle { Width = 4, Height = 6 }
};
foreach (Shape shape in shapes)
{
shape.Draw();
Console.WriteLine("Area: " + shape.Area());
}
}
}
Best Practices
Interview Tip
When discussing polymorphism in an interview, be prepared to explain the different types of polymorphism (runtime and compile-time) and provide examples of how it is achieved through inheritance, interfaces, and generics. Also, be ready to discuss the benefits and drawbacks of each approach. Demonstrate your understanding of the SOLID principles, particularly the Liskov Substitution Principle, which is closely related to polymorphism.
When to Use Polymorphism
Use polymorphism when:
Memory Footprint
The memory footprint of polymorphism can vary depending on the implementation. Inheritance might add a small overhead due to the vtable (virtual function table) used for dynamic dispatch. Interfaces have a minimal memory footprint. Generics generally don't introduce a significant memory overhead because type information is resolved at compile time.
Alternatives
Alternatives to polymorphism include:
Polymorphism offers a much cleaner and more maintainable solution compared to these alternatives.if
or switch
statements to handle different types of objects. This approach can become unwieldy and difficult to maintain as the number of types increases.
Pros of Polymorphism
Cons of Polymorphism
FAQ
-
What is the difference between compile-time and runtime polymorphism?
Compile-time polymorphism (also known as static polymorphism) is resolved at compile time, typically through method overloading or generics. Runtime polymorphism (also known as dynamic polymorphism) is resolved at runtime, typically through inheritance and virtual methods. The correct method to call is determined based on the actual type of the object at runtime.
-
What is the Liskov Substitution Principle?
The Liskov Substitution Principle states that subtypes must be substitutable for their base types without altering the correctness of the program. In other words, if you have a function that operates on a base class, it should also work correctly when you pass in an instance of a derived class.
-
When should I use inheritance vs. interfaces for polymorphism?
Use inheritance when you have a clear 'is-a' relationship between classes and you want to share some common implementation. Use interfaces when you want to define a contract that multiple unrelated classes can implement, and you don't want to share any implementation details.