Method Overloading
By the end of this lesson
Offer several versions of a method and understand how the compiler chooses.
Overloading means several methods sharing one name, told apart by what they accept. Console.WriteLine is the example you have used since your first program: one name, around twenty versions, one for each kind of thing you might write.
The alternative would be WriteLineInt, WriteLineDecimal, WriteLineObject, and a caller having to know which to reach for. Overloading exists so one concept can keep one name.
Console.WriteLine(PayrollLookup.Describe(2041));
Console.WriteLine(PayrollLookup.Describe("P-2041"));
Console.WriteLine(PayrollLookup.Describe(2041, "Payroll"));
public static class PayrollLookup
{
public static string Describe(int employeeId) => $"Employee {employeeId}";
public static string Describe(string payrollNumber) => $"Payroll number {payrollNumber}";
public static string Describe(int employeeId, string department) =>
$"Employee {employeeId} in {department}";
}- The three methods differ in the number and types of their parameters, which is what makes them distinct.
- That parameter list is the signature. Two methods differing only in return type will not compile, because the compiler chooses from the arguments and the return type is not one of them.
- Parameter names are not part of the signature either. Renaming payrollNumber would not create a new overload.
How the compiler picks an overload, in order:
Gather the candidates
Every method with that name whose parameters your arguments could satisfy, allowing for implicit conversions.
Discard what cannot work
Wrong number of arguments, or an argument with no implicit conversion to the parameter type. What is left is the applicable set.
Prefer the closest match
An exact type match beats one needing a conversion. Where both need one, the narrower conversion wins: for an int argument, a long parameter beats a double, because int converts to long and long converts to double.
Prefer a complete signature
A candidate needing no omitted optional arguments is preferred over one that fills in defaults, and a params array used as a list is the last resort.
Fail loudly on a tie
If two candidates are equally good, you get a build error about an ambiguous call rather than a silent choice.
Logger.Log(42); // long — int converts to long more closely than to double
Logger.Log(42.0); // double
Logger.Log("ORD-1041"); // string
object boxed = "ORD-1041";
Logger.Log(boxed); // object — chosen from the declared type, not the value
// Logger.Log(null); // build error: ambiguous between string and int[]
Logger.Log((string?)null); // string — the cast says which you meant
public static class Logger
{
public static void Log(long value) => Console.WriteLine($"long: {value}");
public static void Log(double value) => Console.WriteLine($"double: {value}");
public static void Log(string? value) => Console.WriteLine($"string: {value}");
public static void Log(object? value) => Console.WriteLine($"object: {value}");
public static void Log(int[] values) => Console.WriteLine($"array of {values.Length}");
}- 42 is an int and there is no Log(int). The compiler looks for the best conversion: int to long beats int to double, because a long itself converts to a double and so is the narrower step.
- Log(boxed) picks the object version even though the variable holds a string. Resolution happens while compiling, using the declared type — nothing looks at the value.
- A bare null fits string, object and int[]. Neither string nor int[] is more specific than the other, so the call is ambiguous and the build fails. A cast resolves it.
- The lesson from these three: overloads are chosen by what the compiler can see, not by what happens at run time.
What counts as a different signature:
- A different number of parameters
- Different parameter types
- The same types in a different order — legal, and usually a source of confusion
- Adding ref, out or in where the other has a plain parameter
- Not the return type, and not the parameter names
- Not two overloads differing only in which of ref, out and in they use — that is a build error
Summary
- Overloads share a name and differ by the number, types or order of their parameters
- The return type and parameter names are not part of the signature
- The compiler prefers an exact match, then the narrowest conversion, and reports a tie as an error
- Resolution uses declared types at compile time, so the run-time value never affects the choice
- Overloads should differ in what they accept, not in what they do
Practice
Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.
Try it yourself
Try it yourself
Write three overloads that work out a shipping charge: from weight alone, from weight and membership, and from weight, membership and region.
Have the two shorter versions call the longest one rather than repeating any arithmetic.
Show solution
Only the longest overload contains logic. The others supply the values a caller left out, which means a change to the rate card lands in one place.
That structure also documents the defaults in code rather than in a comment: reading the short overload tells you what "no region given" means.
Compare this with a single method using optional parameters. The behaviour is the same; the difference is that a default can be changed here without every caller being rebuilt, at the cost of three signatures to maintain.
Console.WriteLine(Shipping.Calculate(1.5m));
Console.WriteLine(Shipping.Calculate(1.5m, isMember: true));
Console.WriteLine(Shipping.Calculate(24m, isMember: true, region: "Scotland"));
public static class Shipping
{
public static decimal Calculate(decimal weightKg) =>
Calculate(weightKg, isMember: false, region: "Mainland");
public static decimal Calculate(decimal weightKg, bool isMember) =>
Calculate(weightKg, isMember, region: "Mainland");
public static decimal Calculate(decimal weightKg, bool isMember, string region)
{
decimal baseRate = weightKg <= 2m ? 2.99m : 7.99m;
decimal surcharge = region == "Scotland" ? 1.50m : 0m;
decimal memberDiscount = isMember ? 2m : 0m;
return Math.Max(0m, baseRate + surcharge - memberDiscount);
}
}Think about it
Think about it
A class has Log(long) and Log(double). Calling Log(42) picks the long version.
Explain why, then work out what would happen if the only overloads were Log(double) and Log(string).
Show solution
Both overloads are applicable, because an int converts implicitly to a long and to a double. The compiler prefers the narrower conversion, and int to long is narrower because a long itself converts to a double while the reverse is not implicit.
With only double and string available, string is not applicable at all — there is no implicit conversion from int to string. That leaves one candidate, so Log(double) is chosen and 42 is printed as a double.
If neither were applicable, the result would be a build error rather than a run-time surprise. That is the useful part: overload resolution either finds one best answer or refuses.
Knowledge check
Nothing is recorded and there is no score. The explanation appears either way.
Saved in this browser only.