Back to Blog
C#

C# Method Overloading: Syntax, Rules, and Practical Use

c# method overloading: Learn how C# method overloading works: signature rules, compiler resolution, common mistakes, and when generics or optional parameters fit better.

method overloadingC# syntaxoverload resolutionoptional parametersgeneric methods
Diagram showing multiple C# method signature cards converging into a single resolved overloaded method call

C# method overloading lets you define multiple methods with the same name in a class, as long as each version has a distinct parameter list. The compiler decides which overload to call based on the arguments you pass at the call site.

public class Calculator { public int Add(int left, int right) { return left + right; } public double Add(double left, double right) { return left + right; } }

Here, Add is defined twice. The first version accepts two integers; the second accepts two doubles. When you call calculator.Add(2, 3), the compiler selects the integer version. When you call calculator.Add(2.5, 3.5), it selects the double version. The return type is not part of the overload signature, so you cannot distinguish overloads by return type alone.

What Counts as a Valid Overload

The C# compiler identifies an overload by its method name and parameter list. The parameter list includes the types of the parameters, their order, and whether they are declared ref, out, or in. Two methods with the same name and parameter types but different return types produce a compile error.

// Compile error: cannot overload solely by return type public int GetValue() { return 1; } public string GetValue() { return "one"; }

Parameter names do not matter for overload resolution. The following two declarations conflict:

public void SetPoint(int x, int y) { } public void SetPoint(int left, int right) { } // CS0111: type already defines a member

The order of parameters does matter. SetPoint(int, string) and SetPoint(string, int) are distinct overloads, even though both contain the same two types.

How the Compiler Resolves Overloads

When you call an overloaded method, the compiler evaluates each candidate against the argument types and applies a set of rules defined by the C# language specification to pick the best match. The most important rule is that the compiler prefers the candidate requiring the least costly conversion. An exact match wins over a widening conversion, which wins over a boxing conversion.

public void Process(int value) { } public void Process(long value) { } Process(42); // picks Process(int) - exact match Process(42L); // picks Process(long) - exact match

If you call Process(42) and only the long version exists, the compiler uses the implicit conversion from int to long. If both versions exist, the exact match wins.

When multiple candidates are equally good, the compiler reports an ambiguity error. This commonly happens with null arguments or when mixing optional parameters.

public void Handle(string input) { } public void Handle(object input) { } Handle(null); // CS0121: ambiguous call

Both string and object can accept null, and neither conversion is better than the other, so the call is ambiguous.

Overloading with Optional and Default Parameters

C# also supports optional parameters, which interact with overloading in ways that can surprise developers. When a method declares a default value, the compiler treats it as if it were a separate overload with fewer parameters.

public void Log(string message, string level = "info") { Console.WriteLine($"[{level}] {message}"); } Log("started"); // level defaults to "info" Log("failed", "error"); // explicit level

This works, but combining optional parameters with overloads can create ambiguity. If you define both an overload with one parameter and an overload with two parameters where the second has a default, calls with one argument become ambiguous.

A cleaner approach is to use explicit overloads instead of optional parameters when the number of arguments is small and the behavior differs meaningfully between versions.

public void Send(string message) { Send(message, TimeSpan.FromSeconds(30)); } public void Send(string message, TimeSpan timeout) { // actual implementation }

This pattern keeps the implementation in one place while exposing a convenient one-argument entry point.

Common Mistakes and Edge Cases

One frequent mistake is assuming that overloading works with params arrays the same way it works with fixed parameters. A params parameter is treated as an array for overload resolution, so calls with multiple arguments may not match the way you expect.

public void Join(params string[] parts) { } public void Join(string separator, params string[] parts) { } Join("a", "b", "c"); // ambiguous or picks the second overload

Another edge case involves inheritance. When a derived class defines an overload that hides a base-class method with the same name, the compiler may not consider the base-class overloads unless you use the new keyword or call through a base-typed reference.

public class Base { public void Execute(int value) { } } public class Derived : Base { public void Execute(string value) { } } var d = new Derived(); d.Execute(5); // Compile error: Derived.Execute(string) hides Base.Execute(int)

The derived class only exposes its own Execute(string) overload. The base version is hidden. Adding new to the derived method or using a Base reference restores access to the integer version.

Performance and Maintainability Considerations

Method overloading has no runtime dispatch cost. The compiler resolves the call at compile time, so the generated IL calls the specific method directly. There is no virtual dispatch, reflection, or runtime lookup involved unless the methods are also virtual.

The real cost of overloading is maintainability. Each overload adds surface area to a class, and callers must understand which version applies to their arguments. When overloads grow to more than a handful, consider whether the differences between them are better expressed through a single method that accepts a configuration object.

For example, a method that accepts either a file path or a stream can reasonably be modeled as two overloads:

public void Load(string filePath) { } public void Load(Stream stream) { }

This is reasonable because the two inputs are genuinely different types. But if the overloads differ only by a flag or a default value, a single method with an optional parameter is usually clearer.

When to Prefer Alternatives

Overloading is not always the best tool. C# offers several alternatives that can be more expressive depending on the situation.

Optional parameters work well when the difference between calls is a simple default value. Generic methods are better when the overloads differ only by the type of a single parameter and the body is identical.

public T Max<T>(T left, T right) where T : IComparable<T> { return left.CompareTo(right) >= 0 ? left : right; }

This replaces what would otherwise be a series of near-identical overloads for int, double, string, and so on. The generic version is type-safe and requires no duplication.

Overloading remains the right choice when the method bodies genuinely differ, or when you need to accept unrelated types that do not share a common interface. The decision comes down to whether the variations are about the type of the input or the behavior of the method. If the behavior changes, overload. If only the type changes, consider a generic. If only a default changes, use an optional parameter.

c# method overloading: Practical Usage and Code Examples | RYUSLOG DEV