Back to Blog
C#

C# Overloading vs Overriding: Key Differences

c# overloading vs overriding: Understand the difference between C# overloading and overriding: syntax rules, compile-time vs runtime binding, and when to use each tech...

method overloadingmethod overridingpolymorphismvirtual methodsinheritance
Diagram comparing C# method overloading with compile-time binding and method overriding with runtime dispatch through inheritance.

C# overloading vs overriding is a distinction every C# developer encounters early, yet the two are frequently confused because both involve methods that share a name. Overloading is a compile-time mechanism that lets several methods in the same class share a name while differing in their parameter lists. Overriding is a runtime mechanism that lets a derived class replace the implementation of a virtual method declared in a base class. They solve different problems, follow different rules, and have different effects on how code is dispatched.

Overloading: Multiple Methods, One Name

Method overloading in C# allows a class to declare two or more methods with the same name as long as each has a unique signature. The signature consists of the method name and the number, type, and order of its parameters. The return type is not part of the signature, so you cannot overload solely by changing the return type.

public class PaymentService { public decimal CalculateFee(decimal amount) { return amount * 0.02m; } public decimal CalculateFee(decimal amount, bool isPriority) { return isPriority ? amount * 0.01m : amount * 0.02m; } public decimal CalculateFee(decimal amount, int installments) { return amount * 0.02m + installments * 0.5m; } }

Here, CalculateFee has three overloads. The compiler selects the correct one based on the arguments supplied at the call site. If you call CalculateFee(100m), the first overload runs; if you pass a bool, the second runs; if you pass an int, the third runs. This selection happens entirely at compile time, which means the compiler resolves the call before the application ever runs.

Overloading is useful when the same logical operation can accept different inputs. A constructor is a common example: multiple constructors with different parameter sets let callers initialize an object in different ways. The key constraint is that the parameter lists must differ. Two methods with the same name and parameter types but different return types will not compile.

Overriding: Replacing Inherited Behavior

Overriding is tied to inheritance. A base class marks a method as virtual, and a derived class marks its replacement as override. When the method is called through a base-class reference, the runtime dispatches to the most derived override, even if the caller only knows the static type as the base class.

public class Report { public virtual string Render() { return "Base report"; } } public class PdfReport : Report { public override string Render() { return "PDF report"; } } public class CsvReport : Report { public override string Render() { return "CSV report"; } }

Calling Render() on a Report reference that actually holds a PdfReport returns "PDF report". The decision about which implementation runs is made at runtime, not compile time. This is the foundation of polymorphism in C#: code written against the base type can work with any derived type without knowing the concrete type in advance.

The virtual keyword is required on the base method. Without it, a derived class can declare a method with the same name using new, but that hides the base method rather than overriding it, and dispatch behavior changes accordingly.

The Core Difference: Compile-Time vs Runtime Binding

The most important distinction between overloading and overriding is when the method resolution happens.

Overloading is resolved at compile time. The compiler looks at the static types of the arguments and picks the overload that matches best. There is no runtime lookup, no virtual dispatch table, and no inheritance involved. The overloads all live in the same class, and the compiler can decide the target method before the program executes.

Overriding is resolved at runtime. The runtime uses the virtual method table to find the most derived override for the object's actual type. The static type of the reference does not determine which implementation runs; the runtime type of the object does.

This difference has practical consequences. If you overload a method in a base class and then call it through a derived-class reference, the compiler still selects the overload based on the static type of the reference. Overriding, by contrast, follows the object's actual type regardless of the reference type.

Syntax Rules That Matter

Several rules govern when each technique is valid, and violating them produces compile errors or surprising behavior.

For overloading, the parameter list must differ in at least one of these ways: the number of parameters, the types of parameters, or their order. Parameter names do not matter, and neither does the return type. You can overload methods with ref and out modifiers, but you cannot overload solely by changing ref to out because the compiler treats them as the same signature. Generic methods can be overloaded as long as the parameter lists remain distinguishable.

For overriding, the base method must be marked virtual, abstract, or override. The derived method must use the override keyword and match the base method's signature, including parameter types and return type. Covariant return types are supported in C# 9 and later, allowing the override to return a more derived type, but the parameter list must match exactly. The override cannot reduce accessibility; a public virtual method cannot be overridden as protected.

A common mistake is forgetting the virtual keyword on the base method and then declaring a same-named method in the derived class. The derived method compiles but hides the base method. If the call site uses a base-class reference, the base method runs, not the derived one. This is usually not what the developer intended.

Common Mistakes and Edge Cases

One frequent error is attempting to overload a method by changing only the return type. This fails because the return type is not part of the signature, and the compiler cannot decide which method to call based on the return value alone.

Another mistake is confusing hiding with overriding. When a derived class uses new instead of override, the derived method does not participate in virtual dispatch. Code that holds a base-class reference will call the base version, while code that holds a derived-class reference calls the derived version. This inconsistency is a source of subtle bugs.

Overloading with params arrays can also produce ambiguity. If you have an overload that takes a single int and another that takes params int[], calling with a single integer is unambiguous, but calling with multiple integers may produce a compiler warning or error depending on the exact signatures. The compiler applies a set of rules to determine the best match, and when two overloads are equally applicable, the call is ambiguous.

Inheritance complicates overload resolution further. When a derived class declares an overload that matches a base-class method name, the compiler considers both sets of overloads, but the derived class's overloads take precedence. This can lead to surprising behavior when the derived class adds an overload that shadows base-class overloads.

Choosing the Right Approach for Maintainability

The choice between overloading and overriding is not arbitrary; it follows from the design intent.

Use overloading when you want a single logical operation to accept different input shapes. The operation itself is the same, but the parameters vary. Overloading keeps the API surface small because callers see one method name instead of several differently named methods. It is also appropriate when the variation is known at compile time and does not depend on the runtime type of an object.

Use overriding when you have an inheritance hierarchy and derived types need to provide their own implementation of a base behavior. Overriding is the mechanism that enables polymorphic collections, dependency injection, and framework extension points. If you find yourself adding virtual to every method "just in case," that is a design smell; virtual methods add runtime dispatch overhead and make the base class harder to change without breaking derived implementations.

A pragmatic decision rule: if the method's behavior should vary based on the runtime type of the receiver, use overriding. If it should vary based on the arguments passed in, use overloading. The two are not mutually exclusive. A virtual method can be overloaded, and an override can itself be overloaded in the derived class. The mechanisms operate at different levels and can coexist.

From a maintainability standpoint, overloading is cheaper because overload resolution is static and the compiler catches mismatches early. Overriding introduces coupling between base and derived classes: changing the signature of a virtual method forces every override to change, and adding a new virtual method can affect derived classes that did not anticipate it. Keep the virtual surface small and document the contract that overrides must honor.

c# overloading vs overriding: Practical Usage and Code Examp | RYUSLOG DEV