Skip to main content
ANVISoftware Solutions
Lesson 20 of 62Beginner15 min

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.

Three methods, one name
C#
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:

  1. Gather the candidates

    Every method with that name whose parameters your arguments could satisfy, allowing for implicit conversions.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

Resolution, including the surprising cases
C#
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.

C#
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.

object value = "ORD-1041"; A class has both Log(string) and Log(object). Which one does Log(value) call?

Saved in this browser only.