C# Method Overloading Rules: How Resolution Works
Understand C# method overloading: valid signatures, overload resolution, optional and named arguments, ref/out/in, params arrays, and common ambiguity pitfalls.
When you define multiple methods with the same name in a C# type, the compiler uses a set of rules to decide which one to call. These rules determine whether a method is a valid overload and how overload resolution picks the most specific applicable method. Misunderstanding the rules can lead to ambiguous calls, unexpected overload selection, and code that is harder to maintain.
What Counts as a Method Overload in C#
Method overloading lets you define multiple methods with the same name in the same type as long as their signatures differ. In C#, a method signature includes the method name, the number of type parameters for generic methods, and the types of the parameters, including whether each parameter is passed by value or by reference. The return type and parameter names are not part of the signature.
public class Calculator { public int Add(int a, int b) => a + b; public double Add(double a, double b) => a + b; public int Add(int a, int b, int c) => a + b + c; }
The three Add methods are valid overloads because they differ in parameter types or count. The compiler can determine which method to call from the arguments at the call site.
The Signature Rules That Define an Overload
For two methods to be overloads, they must differ in at least one of the following ways:
- The number of parameters.
- The type of at least one parameter.
- Whether a parameter is passed by value or by reference.
Parameter names are not part of the signature, and params is not part of the signature for overloading purposes. The following are valid overloads:
public void Process(string value) { } public void Process(int value) { } public void Process(string value, int count) { }
The first two differ in parameter type, and the third differs in parameter count. However, the following is not a valid overload:
public string GetValue(int id) => string.Empty; public int GetValue(int id) => 0; // Compiler error: same parameter types
The return type is ignored, so this code will not compile.
How the Compiler Chooses Between Overloads
Overload resolution first removes candidates that are not applicable. A candidate is applicable when the argument count matches after optional parameters are considered, each argument can be converted to the corresponding parameter type, and any required ref, out, or in keywords are present. The compiler then chooses the best applicable candidate using conversion ranking. Exact matches are better than implicit conversions, and more specific parameter types are better than less specific types when both conversions are equally valid. If no candidate is better than the others, the call is ambiguous.
public class Printer { public void Print(int number) { } public void Print(object value) { } } Printer printer = new Printer(); printer.Print(42); // Calls Print(int) because int is more specific than object
Here, int is implicitly convertible to object, but the int overload is a better match because it requires no conversion. If you pass a string, the object overload is the only applicable one.
Overloading with Optional and Named Parameters
Optional parameters complicate overload resolution because they allow a method to be called with fewer arguments than declared. This can create ambiguity when combined with overloads that have different parameter counts.
public class Config { public void Set(int timeout = 30) { } public void Set(int timeout, int retries) { } } Config config = new Config(); config.Set(10); // Calls Set(int timeout = 30) config.Set(10, 3); // Calls Set(int timeout, int retries)
Named arguments can also influence resolution. If you use a name that matches a parameter in one overload but not another, the compiler may select that overload.
public void Draw(int width, int height) { } public void Draw(int size) { } Draw(width: 5, height: 3); // Calls Draw(int, int) Draw(5); // Calls Draw(int)
Optional parameters can lead to ambiguity when two overloads are applicable with the same number of arguments after defaults are applied. For example:
public void Log(string message, int level = 1) { } public void Log(string message, bool verbose = false) { } Log("error"); // Ambiguous: both overloads are applicable with one argument
This call fails to compile because the compiler cannot decide which default to apply.
Overloading with ref, out, and in Parameters
Changing a value parameter to a by-reference parameter changes the signature, so void Modify(int value) and void Modify(ref int value) can coexist. However, ref, out, and in are not three separate signatures. A method cannot define separate overloads that differ only by switching ref int to out int or in int; the compiler treats these as the same by-reference parameter for signature purposes.
public void Modify(int value) { } public void Modify(ref int value) { }
The following is invalid because the methods differ only in the reference modifier:
public void Modify(ref int value) { } public void Modify(out int value) { value = 0; } // Compiler error
When you call a method with a ref argument, the keyword must be present at the call site:
int x = 5; Modify(x); // Calls Modify(int) Modify(ref x); // Calls Modify(ref int)
For out and in, the same principle applies: each has its own calling rules, but none of these reference modifiers by itself creates a distinct overload.
Overloading with params Arrays
The params keyword lets a method accept a variable number of arguments. It is an array in the signature, but the compiler provides special handling at call sites. Because params is not part of the method signature, you cannot pair a parameter array with a plain array overload of the same type:
public void Write(string[] lines) { } public void Write(params string[] lines) { } // Compiler error: same signature
A valid combination uses a different signature for the non-params overload:
public void Sum(params int[] numbers) { } public void Sum(int first, int second) { }
A call like Sum(1, 2) matches the two-parameter overload because it has two explicit parameters. A call like Sum(1, 2, 3) matches the params overload because the two-parameter overload cannot accept three arguments. A call like Sum() matches only the params overload, using an empty array.
Common Ambiguity Errors and How to Avoid Them
Ambiguity errors occur when the compiler cannot determine which overload to use. This often happens with numeric types, inheritance hierarchies, and optional parameters.
Consider this example:
public void Handle(double value) { } public void Handle(decimal value) { } Handle(5); // Ambiguous: int converts to both double and decimal
The int literal can be implicitly converted to both double and decimal, and neither type is a better conversion target. The compiler reports an error. To resolve this, you can cast the argument to a specific type (for example, Handle(5m) or Handle(5d)) or add an overload that accepts int.
Another common source is when a class inherits overloads from a base class. If the derived class defines a method with the same name, it hides the base overloads even if the signatures are different.
public class Base { public void Execute(int value) { } } public class Derived : Base { public void Execute(string value) { } } Derived d = new Derived(); d.Execute(5); // Compiler error: Derived hides Base.Execute(int)
You can call the base method by using a base-typed reference, or from inside the derived class by calling base.Execute(5). The new modifier can document that hiding is intentional, but it does not make the base overload available through the derived type.
Maintainability and Versioning Considerations
Overloading is a powerful tool, but it can hurt maintainability if used excessively. When a method name has many overloads, callers may struggle to find the right one, especially when optional parameters and default values are involved. Clear naming and consistent parameter ordering help reduce confusion.
From a versioning perspective, adding a new overload can silently change which method is selected when consumers recompile. If a caller passes a MemoryStream, a Version 1 API with only Process(Stream data) binds to that method. If Version 2 adds Process(MemoryStream data), the same source call will bind to the new overload after recompilation. Existing compiled assemblies are not re-resolved, so this is not a binary break, but it can change behavior for recompiled callers.
// Version 1 public void Process(Stream data) { } // Version 2 adds this overload public void Process(MemoryStream data) { }
To avoid surprises, consider whether a new overload is truly necessary or whether a differently named method would be clearer. Keep parameter types as general as practical, and document the intended behavior of each overload so future maintainers understand why the overloads exist. Overloading should make the API easier to use, not harder to reason about.