C# Method Overloading vs Overriding: The Difference
Two methods with the same name? That's overloading. A derived class changing a base method? That's overriding.
Imagine you're building a calculator. You want a method called Add that can work with two integers, two decimals, or even three numbers. You don't want to remember AddInts, AddDecimals, AddThree — you just want one name: Add.
Now imagine you're building a payment system. You have a base PaymentProcessor class, and you want a PayPalProcessor that processes payments differently but still follows the same contract.
Overloading and overriding are two of the most important tools in object-oriented programming. They both involve methods with the same name, but they solve fundamentally different problems. Understanding the difference — and when to use each — is essential for writing clean, maintainable C# code.
What Is It?
The Simple Explanation
Overloading — multiple methods in the same class that share the same name but have different parameters. The compiler decides which one to call based on the arguments you provide.
Overriding — a derived class redefines a method from its base class using the override keyword. The runtime decides which implementation to execute based on the actual object type.
The Technical Definition
Overloading
- Compile-time polymorphism (static binding)
- Same method name, different signature (parameter types, count, or order)
- Return type alone does not distinguish overloads
- Can be in the same class or across inheritance
- No
virtual/overriderequired
Overriding
- Runtime polymorphism (dynamic binding)
- Same method name, same signature as base method
- Requires
virtualin base andoverridein derived - Can also use
abstractmethods (must be overridden in non-abstract derived classes) - You can call the base implementation with
base.Method()
Why Does It Exist?
Overloading — The Problem
Without overloading, you'd need separate method names for every variation:
int AddInts(int a, int b) { ... }
double AddDoubles(double a, double b) { ... }
int AddThreeInts(int a, int b, int c) { ... }
decimal AddDecimals(decimal a, decimal b) { ... }
This is hard to remember, hard to read, and makes your API feel clumsy. You want one intuitive name that works naturally with different types and arities.
Overloading — The Solution
Overloading lets you use a single, meaningful name for a family of related operations. The compiler figures out which one to use based on the arguments at compile time.
Overriding — The Problem
Imagine you have a base class with a method that does something useful, but you need slightly different behavior in a derived class. Without overriding, you'd have to hide the base method with new (which breaks polymorphism) or use completely different method names (which defeats inheritance).
Overriding — The Solution
Overriding allows a derived class to replace the base implementation while keeping the same method signature. This enables runtime polymorphism — you can write code that works with the base type, but the derived type's behavior is used at runtime.
Big Picture
virtual/override
Console.WriteLine() has 19 overloads
ToString() is overridden by every type
How It Works
Overloading — Compile-Time Resolution
e.g. Add(5, 10)
- Type:
int, int - Count: 2
- Looks for method named
Addwith matching signature - If multiple match, picks the most specific one
- If no match, compiler error
- The call is fixed at compile time
- No runtime overhead for resolution
Overriding — Runtime Resolution
public virtual string GetGreeting() => "Hello";
public override string GetGreeting() => "G'day";
Base obj = new Derived();
obj.GetGreeting();
- Checks the virtual method table (VMT) of the object
- Finds the most derived override
- Calls that implementation
- Returns
"G'day"even though variable type isBase - This is runtime polymorphism
Simple Example
Overloading
public class Calculator
{
// Overload 1: two ints
public int Add(int a, int b) => a + b;
// Overload 2: two doubles
public double Add(double a, double b) => a + b;
// Overload 3: three ints
public int Add(int a, int b, int c) => a + b + c;
// Overload 4: two decimals
public decimal Add(decimal a, decimal b) => a + b;
}
// Usage
var calc = new Calculator();
Console.WriteLine(calc.Add(5, 10)); // 15 (int, int) → overload 1
Console.WriteLine(calc.Add(5.5, 2.3)); // 7.8 (double, double) → overload 2
Console.WriteLine(calc.Add(1, 2, 3)); // 6 (int, int, int) → overload 3
Console.WriteLine(calc.Add(5.99m, 2.01m)); // 8.00 (decimal, decimal) → overload 4
Code → Meaning → Result
- Four methods all named
Add— each with a different signature. - The compiler chooses the correct one based on the argument types and count.
- This makes the API intuitive — you just call
Addwith whatever numbers you have.
Overriding
public class Animal
{
public virtual string Speak() => "Some animal sound";
}
public class Dog : Animal
{
public override string Speak() => "Woof!";
}
public class Cat : Animal
{
public override string Speak() => "Meow!";
}
// Usage
Animal a1 = new Animal();
Animal a2 = new Dog();
Animal a3 = new Cat();
Console.WriteLine(a1.Speak()); // "Some animal sound"
Console.WriteLine(a2.Speak()); // "Woof!" ← runtime decides!
Console.WriteLine(a3.Speak()); // "Meow!" ← runtime decides!
Code → Meaning → Result
Animaldefines avirtualmethod — a placeholder.DogandCatoverride it with their own implementation.- Even though all variables are typed as
Animal, the runtime calls the correct overridden method based on the actual object type.
Real-World Example
Imagine an e-commerce system with different payment processors. Overloading and overriding work together to make the system flexible and clean.
// ─── Base class with a virtual method ───
public abstract class PaymentProcessor
{
public abstract bool ProcessPayment(decimal amount);
// Overloaded helper methods
public bool ProcessPayment(decimal amount, string currency)
{
// Convert currency if needed, then call the main overload
return ProcessPayment(ConvertToBaseCurrency(amount, currency));
}
public bool ProcessPayment(decimal amount, string currency, string description)
{
LogPayment(description);
return ProcessPayment(amount, currency);
}
protected virtual void LogPayment(string description)
{
Console.WriteLine($"Log: {description}");
}
private decimal ConvertToBaseCurrency(decimal amount, string currency) { ... }
}
// ─── Derived classes override the core method ───
public class StripeProcessor : PaymentProcessor
{
public override bool ProcessPayment(decimal amount)
{
Console.WriteLine($"Stripe: processing {amount:C}");
// Stripe API call...
return true;
}
}
public class PayPalProcessor : PaymentProcessor
{
public override bool ProcessPayment(decimal amount)
{
Console.WriteLine($"PayPal: processing {amount:C}");
// PayPal API call...
return true;
}
// Override logging too
protected override void LogPayment(string description)
{
Console.WriteLine($"PayPal Log: {description.ToUpperInvariant()}");
}
}
// ─── Usage ───
var processors = new List<PaymentProcessor>
{
new StripeProcessor(),
new PayPalProcessor()
};
foreach (var processor in processors)
{
// Even though we only know about PaymentProcessor,
// each processor uses its own implementation!
processor.ProcessPayment(99.99m, "USD", "Order #1234");
}
What's happening here?
- Overriding — each payment processor has its own
ProcessPaymentimplementation. The system works withPaymentProcessorreferences, but the correct processor is used at runtime. - Overloading — the
ProcessPaymentmethod has multiple overloads (amount only, amount+currency, amount+currency+description). This gives callers flexibility while keeping the name consistent. - Together — overloading provides convenience at the call site, while overriding enables polymorphic behavior in the inheritance hierarchy.
Analogy
Overloading — like a multi-tool. You have one tool (the method name) but it comes with several attachments (parameter signatures). You pick the right attachment for the job, and the tool behaves accordingly. The decision is made before you start working (compile time).
Overriding — like a remote control. The base class defines a button (virtual method), and each derived class decides what that button actually does (override). When you press the button, the TV (the runtime) checks what's plugged in and performs the correct action — even if you're pointing the remote at a different room (a base-type reference).
Under the Hood
- Each overload compiles to a separate method with a unique internal name (mangled with parameter types)
- Example:
Add(Int32, Int32)andAdd(Double, Double)are distinct methods in IL - The call site is resolved at compile time — no runtime lookup
- Each class with virtual methods has a VMT — an array of method pointers
- When a derived class overrides a method, it replaces the pointer in the VMT slot
- When you call a virtual method on a reference, the runtime:
- Looks up the object's type
- Finds its VMT
- Calls the method at the correct slot
- This is a single pointer indirection — very fast
- Overloading and overriding work the same way in C# 14 as in earlier versions
- New features like required members and primary constructors don't change these core mechanisms
- Performance improvements in the runtime (e.g., better devirtualization) can optimize virtual calls when the JIT can determine the concrete type
Common Confusion
1. Overloading vs Overriding — The Names Are Similar, the Behaviors Are Not
This is the single biggest source of confusion. Both involve methods with the same name, but they are fundamentally different:
2. Hiding (new) vs Overriding (override)
Using new in a derived class hides the base method — it doesn't override it. When you call the method on a base-type reference, the base implementation runs, not the derived one. This breaks polymorphism and is almost always what you don't want in inheritance hierarchies.
public class Base
{
public virtual void M() => Console.WriteLine("Base.M");
}
public class DerivedNew : Base
{
public new void M() => Console.WriteLine("DerivedNew.M"); // hides!
}
public class DerivedOverride : Base
{
public override void M() => Console.WriteLine("DerivedOverride.M");
}
Base b1 = new DerivedNew();
Base b2 = new DerivedOverride();
b1.M(); // "Base.M" ← new doesn't override
b2.M(); // "DerivedOverride.M" ← override works
3. Overloading and Return Types
Return type alone does NOT distinguish overloads. You cannot have two methods with the same name and same parameters but different return types — the compiler can't know which one to call.
// Does NOT compile
public int GetValue() => 42;
public string GetValue() => "42"; // Compiler error!
Common Mistakes
Mistake 1 — Forgetting override keyword
Wrong — this hides the method instead of overriding it:
public class Dog : Animal
{
public string Speak() => "Woof!"; // Hides, doesn't override!
}
Correct — use override to get polymorphic behavior:
public class Dog : Animal
{
public override string Speak() => "Woof!";
}
Mistake 2 — Overloading with ambiguous parameters
Creating overloads that can cause ambiguity:
public void M(int a, double b) { }
public void M(double a, int b) { }
// M(1, 2) → ambiguous! Compiler error.
Design overloads where the compiler can unambiguously choose based on argument types.
Mistake 3 — Forgetting to call base when needed
When overriding, sometimes you need the base behavior to run in addition to your new behavior. Forgetting base.Method() can break important setup logic.
public override void Dispose()
{
// Missing: base.Dispose();
// Now base resources are never cleaned up!
}
When Should I Use It?
Use overloading when:
- You need the same logical operation to work with different types or different numbers of parameters
- You want to provide convenience to callers (e.g.,
Console.WriteLinehas 19 overloads so you can print almost anything) - You're designing a fluent API or a helper class
- You want to provide default values via overloads (e.g., one overload with 2 params, another with 3 where the third has a default)
Use overriding when:
- You have a base class with behavior that should be customizable or replaceable in derived classes
- You're implementing a template method pattern where base class calls virtual methods
- You need runtime polymorphism — working with base-type references but getting derived behavior
- You're building a framework or library where users can extend your classes
- You need to provide a different implementation of
ToString(),Equals(), orGetHashCode()in your custom type
When to avoid over-using:
- Too many overloads can make the API confusing — use defaults and optional parameters where appropriate
- Overriding everything by default can make design fragile — prefer
sealedfor classes/methods that shouldn't be extended - If you're not using inheritance, you don't need overriding — composition over inheritance is often a better design
Mental Model
Overriding = same name, same parameters → runtime picks
Remember:
· Overloading is about convenience — one name, many forms
· Overriding is about polymorphism — one interface, many behaviors
· Overloading is resolved at compile time
· Overriding is resolved at runtime
· You can overload without inheritance; you can't override without it
Key Takeaway
- Overloading — multiple methods, same name, different parameters. Compile-time choice. Provides convenience and clarity.
- Overriding — derived class replaces a base class method. Runtime choice. Provides polymorphism and extensibility.
- Overloading requires different signatures; overriding requires the exact same signature.
- Overriding needs
virtualin the base andoverridein the derived class — don't forget them. - Use
base.Method()to call the base implementation when you need to extend, not replace, behavior. - Hiding (
new) is not overriding — it breaks polymorphism and is rarely what you want.
Check Your Understanding
You've seen how overloading and overriding work, how they differ, and when to use each. Let's see if you can apply these concepts.
1. Which of the following correctly describes the difference between overloading and overriding?
Show answer
Correct: C
Why C is correct: Overloading is about having multiple methods with the same name but different parameters. Overriding is about replacing a base class method's implementation in a derived class with the same signature. This is the core distinction.
Why A is incorrect: It's the opposite — overloading is resolved at compile time (static binding), and overriding is resolved at runtime (dynamic binding).
Why B is incorrect: Overriding requires inheritance (you must have a base class with a virtual/abstract method). Overloading does not require inheritance — you can overload methods in the same class.
Why D is incorrect: Return type alone cannot distinguish overloads, but overloads can have different return types as long as the parameter signatures differ. Overriding requires the exact same return type (or a covariant return type in some languages, but in C# it must match).
Reinforcement: Overloading = compile-time, same name different params. Overriding = runtime, same name same params with virtual/override.
2. Consider the following code. What is the output?
public class Shape
{
public virtual void Draw() => Console.WriteLine("Shape");
}
public class Circle : Shape
{
public override void Draw() => Console.WriteLine("Circle");
}
public class Square : Shape
{
public new void Draw() => Console.WriteLine("Square");
}
Shape s1 = new Circle();
Shape s2 = new Square();
s1.Draw();
s2.Draw();
Show answer
Correct: B
Why B is correct: Circle uses override, so when called on a Shape reference, the runtime calls Circle.Draw. Square uses new (hiding), so when called on a Shape reference, the base Shape.Draw runs.
Why A is incorrect: This would happen if s2 was declared as Square instead of Shape. But here the variable is a Shape reference, and new does not override.
Why C and D are incorrect: They incorrectly predict Shape for s1, but Circle uses override so polymorphism works.
Reinforcement: override enables runtime polymorphism; new hides the base method and does not.
3. Which of the following method signatures can coexist as overloads in the same class?
Show answer
Correct: D
Why D is correct: Both B and C demonstrate valid overloading. B changes the parameter type (int vs double). C changes the parameter count (2 vs 3).
Why A is incorrect: These two methods have the exact same signature (M(int, int)) and differ only by return type. Return type alone cannot distinguish overloads in C# — the compiler cannot know which one to call.
Why C is not the only correct answer: C is valid, but B is also valid, so D is the best answer.
Reinforcement: Overloads must differ by parameter type, count, or order — not by return type alone.
4. In a real application, you have a base ReportGenerator class with a virtual Generate() method. You need a PdfReportGenerator that produces PDF output and a CsvReportGenerator that produces CSV. You want to write code that works with ReportGenerator references but gets the correct format. Which approach should you use?
Show answer
Correct: B
Why B is correct: Overriding is the correct tool when you want derived classes to replace the behavior of a base class method while keeping the same signature. This enables runtime polymorphism — you can write code that works with ReportGenerator and the correct generation happens automatically.
Why A is incorrect: Overloading with different parameters would break the contract — callers would need to know which overload to use, defeating the purpose of polymorphic design.
Why C is incorrect: new hides the base method — when calling through a base reference, the base implementation runs, not the derived one. This breaks polymorphism.
Why D is incorrect: Creating separate methods defeats the purpose of inheritance and forces callers to know about each concrete type.
Reinforcement: When you need different behavior in derived classes while keeping a common interface, override is the way to go.
5. You're designing a logging library. You want a Log method that can accept a string message, a message with an exception, or a message with structured data. Users should be able to call logger.Log("Hello"), logger.Log("Error", ex), and logger.Log("Event", new { UserId = 123 }). What should you use?
Show answer
Correct: C
Why C is correct: In a real logging library, you typically use overloading to provide convenience methods for callers (different parameter combinations). Inside those overloads, you call a single core Log method that is virtual so that derived loggers (e.g., FileLogger, DatabaseLogger) can override the core behavior while preserving the convenient overloads. This is a common and powerful pattern.
Why A is incorrect: Overriding alone doesn't give you multiple parameter combinations — you'd need overloading for that.
Why B is incorrect: Overloading alone works for the convenience part, but if you want different logger implementations (e.g., file, database, console) to handle logging differently, you also need overriding.
Why D is incorrect: Separate method names are less intuitive and force callers to remember different names. The whole point of overloading is to keep a consistent, discoverable API.
Reinforcement: Overloading provides convenience at the call site. Overriding provides polymorphic behavior across derived classes. They are not mutually exclusive — they work beautifully together.
You now have a solid mental model of overloading and overriding — you can distinguish them, use them correctly, and explain why they exist!