Method Overloading in C# (With Real-Time Examples)

Say you’re writing the checkout for an Austin outdoor store, and method overloading in C# is how one Calculate method takes whole dollars, a price with cents, or a price plus a custom tax rate.

Quick answer: declare several methods with the same name but a different number, type or order of parameters. The compiler picks one from the arguments at compile time. A different return type alone won’t compile.

I built that tax calculator on .NET 10, then broke it on purpose so you can see the real errors.

What Is Method Overloading in C#?

Method overloading means one class has two or more methods with the same name and different parameter lists. Microsoft calls the name plus the parameter list the method’s signature, and every overload needs a unique one.

It’s the compile-time half of polymorphism: the compiler decides which version runs before the program starts. That’s how the four pillars fit together in my OOP concepts in C# guide.

You already use overloads. Console.WriteLine has 20 of them on .NET 10, so you can pass an int, a string or a decimal without calling WriteInt or WriteString.

Real-Time Example: A Sales Tax Calculator with Overloaded Methods

Here’s the running sample. The store charges Austin’s combined 8.25% rate, and the SalesTax class has four Calculate overloads:

using System.Globalization;
CultureInfo.CurrentCulture = new("en-US");

var tax = new SalesTax();
Console.WriteLine(tax.Calculate(40));
Console.WriteLine(tax.Calculate(39.99m));
Console.WriteLine(tax.Calculate(39.99m, 0.0625m));
Console.WriteLine(tax.Calculate(3, 39.99m));

class SalesTax
{
    const decimal Austin = 0.0825m;
    public string Calculate(int dollars) => $"(int)              {dollars * (1 + Austin):C}";
    public string Calculate(decimal price) => $"(decimal)          {price * (1 + Austin):C}";
    public string Calculate(decimal price, decimal rate) => $"(decimal, decimal) {price * (1 + rate):C}";
    public string Calculate(int qty, decimal price) => $"(int, decimal)     {qty * price * (1 + Austin):C}";
}

Output:

(int)              $43.30
(decimal)          $43.29
(decimal, decimal) $42.49
(int, decimal)     $129.87
Method overloading in C# sales tax calculator with four Calculate overloads and output in VS Code
Each call lands on a different Calculate overload, picked only by the arguments you pass.

The int call returns $43.30 and the decimal call returns $43.29 for $39.99. The two-argument versions differ only by type, so (3, 39.99m) goes to the quantity overload and totals $129.87 for three items.

Give overloads one name only when they do one job. Verb-first names like Calculate are in my C# method naming conventions.

Method Overloading Rules: What Counts as a Different Signature?

Two methods are valid overloads when at least one of these differs. Everything in the last three rows is ignored:

ChangeValid overload?Example
Number of parametersYesCalculate(decimal) / Calculate(decimal, decimal)
Type of parametersYesCalculate(int) / Calculate(decimal)
Order of parameter typesYesCalculate(int, decimal) / Calculate(decimal, int)
By value vs ref/out/inYesRound(decimal) / Round(ref decimal)
ref vs out (or in)No, CS0663Round(ref decimal) / Round(out decimal)
Return type onlyNo, CS0111decimal Calculate(decimal) / string Calculate(decimal)
Parameter names or access modifierNo, CS0111Calculate(decimal price) / Calculate(decimal amount)

Generic methods count by the number of type parameters, so Map<T>() and Map() can live side by side.

Why can’t you overload by return type alone?

The compiler picks an overload from the call, and tax.Calculate(39.99m) says nothing about the type you want back. So a return-type-only pair is a duplicate, and ref vs out is rejected too:

Console.WriteLine("This file never builds");

class SalesTax
{
    public decimal Calculate(decimal price) => price * 1.0825m;
    public string Calculate(decimal price) => $"{price * 1.0825m:0.00}";

    public void Round(ref decimal amount) { }
    public void Round(out decimal amount) => amount = 0m;
}

Build output:

C:\DotNetDemo\OverloadingDemo\DuplicateOverloads.cs(6,19): error CS0111: Type 'SalesTax' already defines a member called 'Calculate' with the same parameter types
C:\DotNetDemo\OverloadingDemo\DuplicateOverloads.cs(9,17): error CS0663: 'SalesTax' cannot define an overloaded method that differs only on parameter modifiers 'out' and 'ref'

The build failed. Fix the build errors and run again.
C# overloading by return type only gives error CS0111 and ref vs out gives CS0663 in VS Code
CS0111 for the return-type-only pair and CS0663 for ref vs out. The build stops before anything runs.

Local functions can’t be overloaded

This one catches people writing .NET 10 file-based apps. Top-level static functions are local functions, and they can’t share a name:

Console.WriteLine(Area(3));
Console.WriteLine(Area(3, 4));

static int Area(int side) => side * side;
static int Area(int width, int height) => width * height;

Build output:

C:\DotNetDemo\OverloadingDemo\LocalFunctions.cs(2,19): error CS1501: No overload for method 'Area' takes 2 arguments
C:\DotNetDemo\OverloadingDemo\LocalFunctions.cs(5,12): error CS0128: A local variable or function named 'Area' is already defined in this scope
C:\DotNetDemo\OverloadingDemo\LocalFunctions.cs(5,12): warning CS8321: The local function 'Area' is declared but never used

The build failed. Fix the build errors and run again.

Move them into a class, like Printer or SalesTax in the other examples, and the overloads work. The difference between local functions and methods is in my functions in C# tutorial.

Overload Resolution: How Does C# Pick an Overload?

The compiler collects every overload your arguments can convert to and keeps the best conversion. An exact match beats widening, and widening beats object.

short s = 5;
Printer.Show(5);
Printer.Show(5L);
Printer.Show(5.0);
Printer.Show(5f);
Printer.Show(s);
Printer.Show(5m);

static class Printer
{
    public static void Show(int n) => Console.WriteLine($"{n,-3} -> Show(int)");
    public static void Show(long n) => Console.WriteLine($"{n,-3} -> Show(long)");
    public static void Show(double n) => Console.WriteLine($"{n,-3} -> Show(double)");
    public static void Show(object n) => Console.WriteLine($"{n,-3} -> Show(object)");
}

Output:

5   -> Show(int)
5   -> Show(long)
5   -> Show(double)
5   -> Show(double)
5   -> Show(int)
5   -> Show(object)
C# overload resolution picks Show(int), Show(long), Show(double) or Show(object) for literals in VS Code
The literal’s type decides: 5 is int, 5L is long, 5f widens to double and 5m only fits object.

Three results surprise people. A float goes to double, a short goes to int, and decimal skips both numeric overloads because there’s no implicit conversion from it.

The type casting in C# post lists every implicit numeric conversion, which is exactly the table overload resolution uses.

The null ambiguity error (CS0121)

Add a Lookup(string) and a Lookup(int[]) overload, then pass null. Both are reference types, so neither is better:

var tax = new SalesTax();
tax.Lookup(null);

class SalesTax
{
    public void Lookup(string zip) => Console.WriteLine("string");
    public void Lookup(int[] zips) => Console.WriteLine("int[]");
}

Build output:

C:\DotNetDemo\OverloadingDemo\AmbiguousNull.cs(2,5): error CS0121: The call is ambiguous between the following methods or properties: 'SalesTax.Lookup(string)' and 'SalesTax.Lookup(int[])'

The build failed. Fix the build errors and run again.
C# error CS0121 the call is ambiguous when null matches Lookup(string) and Lookup(int[]) in VS Code
null converts to both string and int[], so the compiler refuses to guess.

Cast it, tax.Lookup((string?)null), or better, don’t pass null at all. My null checks in C# post shows the guard clauses I use instead.

Optional Parameters vs Method Overloading

An optional parameter can replace an overload: Ship(decimal lbs, string speed = "ground") covers both calls. Mix the two and you’ll get a surprise:

using System.Globalization;
CultureInfo.CurrentCulture = new("en-US");

Order.Ship(25m);
Order.Ship(25m, "2-day");
Order.Total(19.99m, 5.25m, 40m);
Order.Total([12.50m, 7.50m]);

static class Order
{
    public static void Ship(decimal lbs) => Console.WriteLine("Ship(decimal)");
    public static void Ship(decimal lbs, string speed = "ground")
        => Console.WriteLine($"Ship(decimal, string speed = \"{speed}\")");

    public static void Total(params decimal[] prices)
        => Console.WriteLine($"params decimal[]       {prices.Sum():C}");
    public static void Total(params ReadOnlySpan<decimal> prices)
    {
        decimal sum = 0;
        foreach (var p in prices) sum += p;
        Console.WriteLine($"params ReadOnlySpan    {sum:C}");
    }
}

Output:

Ship(decimal)
Ship(decimal, string speed = "2-day")
params ReadOnlySpan    $65.24
params ReadOnlySpan    $20.00
C# optional parameters vs method overloading and params ReadOnlySpan overload in VS Code
Ship(25m) picks the overload without the optional parameter. Both Total calls go to the C# 13 params span overload.

Ship(25m) calls the one-parameter version, because the tie-breaker prefers a method with no omitted optional parameters.

Here’s my rule: use optional parameters for simple defaults, and use overloads when the types differ or another .NET language calls your library, since a changed default is baked into the caller at compile time.

params arrays and C# 13 params collections

A params parameter takes any number of arguments. Since C# 13, it can be a ReadOnlySpan<T>, List<T> or interface like IEnumerable<T>, not only an array.

With both overloads present, the span version won for three loose prices ($65.24) and for a collection expression ($20.00). That’s why .NET 10 APIs add span overloads: no array gets allocated. The arrays in C# guide covers the array side.

Constructor Overloading and Operator Overloading

Constructors overload exactly like methods. This Money struct accepts whole dollars, dollars and cents, or text, and also overloads the + operator:

using System.Globalization;
CultureInfo.CurrentCulture = new("en-US");

var a = new Money(19);
var b = new Money(19, 99);
var c = new Money("4.25");
Console.WriteLine($"{a} + {b} + {c} = {a + b + c}");

readonly struct Money
{
    public decimal Amount { get; }
    public Money(int dollars) : this(dollars, 0) { }
    public Money(int dollars, int cents) => Amount = dollars + cents / 100m;
    public Money(string text) => Amount = decimal.Parse(text, CultureInfo.InvariantCulture);
    private Money(decimal amount) => Amount = amount;
    public static Money operator +(Money x, Money y) => new(x.Amount + y.Amount);
    public override string ToString() => Amount.ToString("C");
}

Output:

$19.00 + $19.99 + $4.25 = $43.24

: this(dollars, 0) chains one constructor to another, so the math lives in one place. Chaining and primary constructors are in my constructors in C# guide.

Operator overloading is a different feature: a public static method named operator + that lets a + b work on your type. Keep it for math-like types such as money; structs in C# are the usual home for it.

Method Overloading in a Derived Class

Overloads can be split across a base and derived class. The catch: the compiler looks at the derived class first and stops there if anything fits.

var store = new OnlineStore();
store.Discount(10);
((Store)store).Discount(10);

class Store
{
    public void Discount(int percent) => Console.WriteLine($"Store.Discount(int {percent})");
}
class OnlineStore : Store
{
    public void Discount(double percent) => Console.WriteLine($"OnlineStore.Discount(double {percent})");
}

Output:

OnlineStore.Discount(double 10)
Store.Discount(int 10)

store.Discount(10) ran the double version even though the base has an exact int match. Through a Store variable, the int version runs. The inheritance in C# post explains how base and derived members are looked up.

Method Overloading vs Method Overriding in C#

OverloadingOverriding
SignatureSame name, different parametersSame name and same parameters
WhereSame class (or base + derived)Derived class only
KeywordsNonevirtual/abstract + override
DecidedCompile timeRun time, by the object’s type
PolymorphismCompile-time (static)Runtime (dynamic)
Return typeCan differMust match (or be covariant)

Interfaces add a third case: a class can implement two interfaces in C# that each declare an overload. For the runtime side, read my method overriding in C# tutorial.

Common Overloading Mistakes and How to Avoid Them

  • Overloads that do different jobs. Calculate(int) should not secretly send an email. Same name, same job.
  • Too many overloads. Past four or five, a parameter object or optional parameters reads better.
  • Counting on int vs double. Calculate(40) and Calculate(40m) gave different totals above. Pick one numeric type for money: decimal.
  • Overloads with null-friendly types. Two reference-type overloads plus a null argument means CS0121.

If you’re working through OOP, these pair well with overloading:

Frequently Asked Questions

Is method overloading compile-time or runtime polymorphism?

Compile-time polymorphism. The compiler picks the overload from the argument types when it builds your code, while overriding is resolved at run time.

Can I overload a method by return type in C#?

No. Two methods that differ only in return type give error CS0111, because the call doesn’t say which return type you want. Change the parameter list or the name.

Can static methods be overloaded?

Yes. Static methods overload the same way as instance methods, as the Printer and Order classes in this article show. Local functions inside a method can’t be overloaded.

Can a method be overloaded in a derived class?

Yes, but the compiler checks the derived class’s applicable methods first. A derived Discount(double) wins over a base Discount(int) when you call through the derived type.

Should I use optional parameters or overloads?

Optional parameters suit simple defaults inside your own app. Overloads are safer for public libraries, because a changed default value is compiled into every caller.

Where are the official overloading rules?

Microsoft documents the overload errors CS0111, CS0121 and CS0663 on Fix errors that involve overload resolution on Microsoft Learn.