Back to Blog
C#

C# Overloading vs Overriding: Key Differences

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

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 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. For statically typed arguments, this selection happens 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 normally resolved at compile time. For statically typed arguments, the compiler looks at the static types of the arguments and picks the overload that matches best. There is no virtual dispatch and no lookup based on the receiver's runtime type. Overloads can be declared in one class, and inherited methods can participate in overload resolution. The compiler can decide the target method before the program executes for statically typed calls; if any argument is declared as dynamic, overload resolution is deferred until runtime.

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 call it through a derived-class reference, the compiler still selects the overload using the static types of the arguments, not the runtime type of the object. 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. The ref and out modifiers do not make two signatures different from each other: 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.

Overload resolution can also report ambiguity. The compiler applies a set of rules to choose the best match, and when two overloads are equally applicable, the call is ambiguous. A common case is passing a null literal to overloads whose parameters are different reference types; the compiler cannot prefer one conversion over the other.

Inheritance can also complicate overload resolution. A method declared in a derived class with the same signature as a base-class method hides the base method unless it is an override. If the signatures are genuinely different, both methods remain available and overload resolution selects the best match.

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 Examples | RYUSLOG DEV