Polymorphism
Polymorphism means “many forms.” It allows objects of different types to be treated through a common interface, with each type providing its own implementation.
Types of Polymorphism
C# supports several types of polymorphism:
- Compile-time (Static): Method overloading, operator overloading
- Runtime (Dynamic): Method overriding, interface implementation
- Parametric: Generics
Compile-Time Polymorphism: Method Overloading
The compiler determines which method to call based on the method signature.
public class Calculator
{
// Same method name, different parameter types
public int Add(int a, int b) => a + b;
public double Add(double a, double b) => a + b;
public string Add(string a, string b) => a + b;
// Same method name, different number of parameters
public int Add(int a, int b, int c) => a + b + c;
public int Add(params int[] numbers) => numbers.Sum();
}
// Usage - compiler picks the right method
var calc = new Calculator();
int sum1 = calc.Add(1, 2); // Calls Add(int, int)
double sum2 = calc.Add(1.5, 2.5); // Calls Add(double, double)
string concat = calc.Add("Hello", "World"); // Calls Add(string, string)
int sum3 = calc.Add(1, 2, 3, 4, 5); // Calls Add(params int[])
Runtime Polymorphism: Method Overriding
The runtime determines which method to call based on the actual object type.
public abstract class Shape
{
public string Color { get; set; }
public abstract double GetArea();
public abstract void Draw();
}
public class Circle : Shape
{
public double Radius { get; set; }
public override double GetArea() => Math.PI * Radius * Radius;
public override void Draw() => Console.WriteLine($"Drawing {Color} circle");
}
public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public override double GetArea() => Width * Height;
public override void Draw() => Console.WriteLine($"Drawing {Color} rectangle");
}
public class Triangle : Shape
{
public double Base { get; set; }
public double Height { get; set; }
public override double GetArea() => 0.5 * Base * Height;
public override void Draw() => Console.WriteLine($"Drawing {Color} triangle");
}
// Polymorphic usage - same method, different behavior
public class ShapeProcessor
{
public void ProcessShapes(List<Shape> shapes)
{
foreach (var shape in shapes)
{
shape.Draw(); // Calls the correct Draw() for each shape type
Console.WriteLine($"Area: {shape.GetArea()}");
}
}
}
// Usage
var shapes = new List<Shape>
{
new Circle { Radius = 5, Color = "Red" },
new Rectangle { Width = 4, Height = 3, Color = "Blue" },
new Triangle { Base = 6, Height = 4, Color = "Green" }
};
var processor = new ShapeProcessor();
processor.ProcessShapes(shapes);
// Output:
// Drawing Red circle
// Area: 78.54
// Drawing Blue rectangle
// Area: 12
// Drawing Green triangle
// Area: 12
Interface Polymorphism
Different classes implement the same interface differently.
public interface IPaymentMethod
{
Task<PaymentResult> ProcessAsync(decimal amount);
Task RefundAsync(decimal amount);
string PaymentType { get; }
}
public class CreditCard : IPaymentMethod
{
public string PaymentType => "Credit Card";
public string CardNumber { get; set; }
public async Task<PaymentResult> ProcessAsync(decimal amount)
{
Console.WriteLine($"Charging ${amount} to card ending in {CardNumber[^4..]}");
return new PaymentResult { Success = true };
}
public async Task RefundAsync(decimal amount)
{
Console.WriteLine($"Refunding ${amount} to card");
}
}
public class PayPal : IPaymentMethod
{
public string PaymentType => "PayPal";
public string Email { get; set; }
public async Task<PaymentResult> ProcessAsync(decimal amount)
{
Console.WriteLine($"Charging ${amount} via PayPal to {Email}");
return new PaymentResult { Success = true };
}
public async Task RefundAsync(decimal amount)
{
Console.WriteLine($"Refunding ${amount} via PayPal");
}
}
public class Cryptocurrency : IPaymentMethod
{
public string PaymentType => "Crypto";
public string WalletAddress { get; set; }
public async Task<PaymentResult> ProcessAsync(decimal amount)
{
Console.WriteLine($"Processing ${amount} crypto payment");
return new PaymentResult { Success = true };
}
public async Task RefundAsync(decimal amount)
{
Console.WriteLine($"Refunding ${amount} to wallet");
}
}
// Payment gateway doesn't need to know specific payment types
public class PaymentGateway
{
public async Task ProcessPaymentAsync(IPaymentMethod method, decimal amount)
{
Console.WriteLine($"Processing {method.PaymentType} payment...");
var result = await method.ProcessAsync(amount);
if (!result.Success)
{
Console.WriteLine("Payment failed, initiating refund...");
await method.RefundAsync(amount);
}
}
}
Generic Polymorphism (Parametric)
Write code that works with any type while maintaining type safety.
public interface IRepository<T> where T : class
{
Task<T> GetByIdAsync(int id);
Task<List<T>> GetAllAsync();
Task AddAsync(T entity);
Task UpdateAsync(T entity);
Task DeleteAsync(int id);
}
public class Repository<T> : IRepository<T> where T : class
{
protected readonly DbContext _context;
public Repository(DbContext context)
{
_context = context;
}
public async Task<T> GetByIdAsync(int id)
=> await _context.Set<T>().FindAsync(id);
public async Task<List<T>> GetAllAsync()
=> await _context.Set<T>().ToListAsync();
public async Task AddAsync(T entity)
{
await _context.Set<T>().AddAsync(entity);
await _context.SaveChangesAsync();
}
public async Task UpdateAsync(T entity)
{
_context.Set<T>().Update(entity);
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(int id)
{
var entity = await GetByIdAsync(id);
if (entity != null)
{
_context.Set<T>().Remove(entity);
await _context.SaveChangesAsync();
}
}
}
// Same repository code, different types
var userRepo = new Repository<User>(context);
var productRepo = new Repository<Product>(context);
var orderRepo = new Repository<Order>(context);
// All share the same interface
await userRepo.GetAllAsync();
await productRepo.AddAsync(new Product { Name = "Widget" });
Polymorphism in Action: Plugin Architecture
// Define a plugin interface
public interface IExportPlugin
{
string Format { get; }
byte[] Export(ReportData data);
}
// Different export implementations
public class PdfExporter : IExportPlugin
{
public string Format => "PDF";
public byte[] Export(ReportData data)
{
// PDF generation logic
return GeneratePdf(data);
}
}
public class ExcelExporter : IExportPlugin
{
public string Format => "Excel";
public byte[] Export(ReportData data)
{
// Excel generation logic
return GenerateExcel(data);
}
}
public class CsvExporter : IExportPlugin
{
public string Format => "CSV";
public byte[] Export(ReportData data)
{
// CSV generation logic
return GenerateCsv(data);
}
}
// Report service supports any export format
public class ReportService
{
private readonly Dictionary<string, IExportPlugin> _exporters;
public ReportService(IEnumerable<IExportPlugin> exporters)
{
_exporters = exporters.ToDictionary(e => e.Format);
}
public byte[] GenerateReport(ReportData data, string format)
{
if (!_exporters.TryGetValue(format, out var exporter))
throw new NotSupportedException($"Format {format} not supported");
return exporter.Export(data);
}
}
// Add new formats without changing ReportService!
Benefits of Polymorphism
| Benefit | Description |
|---|---|
| Flexibility | Easy to add new types without changing existing code |
| Loose Coupling | Code depends on abstractions, not concrete types |
| Maintainability | Changes isolated to specific implementations |
| Testability | Mock interfaces for unit testing |
| Extensibility | New implementations don’t break callers |
Interview Tips
Common Questions:
- “Explain polymorphism with an example”
- “What’s the difference between overloading and overriding?”
- “How does polymorphism support the Open/Closed Principle?”
Key Points:
- Overloading (compile-time): Same name, different parameters
- Overriding (runtime): Same signature, different implementation in derived class
- Interface polymorphism: Different classes, same interface
- Generics: Type-agnostic code
Interview Code Challenge:
// Classic polymorphism question: What's the output?
public class Animal { public virtual void Speak() => Console.WriteLine("Animal"); }
public class Dog : Animal { public override void Speak() => Console.WriteLine("Dog"); }
public class Cat : Animal { public override void Speak() => Console.WriteLine("Cat"); }
Animal a = new Dog();
a.Speak(); // Output: "Dog" (runtime polymorphism)
Animal[] animals = { new Dog(), new Cat(), new Animal() };
foreach (var animal in animals)
animal.Speak(); // Output: "Dog", "Cat", "Animal"