C# Method Overloading

Method overloading means defining multiple C# methods with the same name but different parameter lists. It matters because it lets one operation have a natural name while still accepting different kinds or amounts of input.

For example, Console.WriteLine is overloaded so you can print a string, an int, a double, and many other values with the same method name. The compiler chooses the specific overload before your program runs.

Overview: How Method Overloading Works

An overload set is a group of methods with the same name in the same type or inheritance context. Each overload must have a different parameter list. In C#, the method name plus parameter types and parameter modifiers form the important part of the signature used for overloading. The return type alone is not enough, and parameter names alone are not enough.

When the compiler sees a call such as FormatValue(42), it does not simply search for the first method named FormatValue. It gathers all visible overloads named FormatValue, compares the arguments with each parameter list, removes candidates that cannot accept the arguments, and then chooses the best remaining match. If no candidate works, the code does not compile. If two or more candidates are equally good, the call is ambiguous and also does not compile.

Overload selection is mostly a compile-time decision. That means the declared compile-time type of an expression matters. If a variable is declared as object but contains a string at runtime, an overload call using that variable is resolved from the type object, not from the hidden runtime value. Runtime polymorphism with virtual methods is a separate feature; overloading chooses the method shape, while overriding chooses an implementation for an already chosen virtual method.

Different overloads can vary by number of parameters, parameter types, parameter order, and certain parameter modifiers such as ref, out, and in. However, overloading is most readable when all overloads represent the same concept. If two methods do unrelated jobs, giving them the same name makes the API harder to understand even if the compiler allows it.

The CLR metadata stores each method with its name and signature, so overloaded methods are distinct methods after compilation. The compiler emits a call to the exact overload it selected. There is no repeated searching by name every time the line executes.

Syntax

static returnType MethodName(type parameter)
{
    statements;
}

static returnType MethodName(otherType parameter)
{
    statements;
}

static returnType MethodName(type first, type second)
{
    statements;
}
Part Meaning
MethodName The shared name. All overloads in the overload set use this name.
type parameter The parameter list. It must differ from the other overloads by type, count, order, or allowed modifier.
returnType The result type. It can differ between overloads, but it cannot be the only difference.
statements The body for that specific overload. Each overload has its own implementation.

The most common overloads differ by parameter count or by clearly different parameter types. Avoid overload sets where the caller must study several similar signatures to guess which method will run.

Examples

Overloading by Parameter Type

using System;
using System.Globalization;

class Program
{
    static void Main()
    {
        Console.WriteLine(FormatValue(42));
        Console.WriteLine(FormatValue(19.5m));
        Console.WriteLine(FormatValue(new DateTime(2026, 7, 24)));
    }

    static string FormatValue(int value)
    {
        return $"whole number: {value}";
    }

    static string FormatValue(decimal value)
    {
        return "money: $" + value.ToString("0.00", CultureInfo.InvariantCulture);
    }

    static string FormatValue(DateTime value)
    {
        return "date: " + value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
    }
}

Output:

whole number: 42
money: $19.50
date: 2026-07-24

All three methods are named FormatValue, but their parameter types are different. The argument 42 is an int, 19.5m is a decimal, and new DateTime(...) is a DateTime, so each call has a clear best match.

Overloading by Number of Parameters

using System;

class Program
{
    static void Main()
    {
        Console.WriteLine(CreateLabel("Notebook"));
        Console.WriteLine(CreateLabel("Backpack", "Aisle 3"));
        Console.WriteLine(CreateLabel("Laptop", "Secure Cabinet", true));
    }

    static string CreateLabel(string item)
    {
        return item;
    }

    static string CreateLabel(string item, string location)
    {
        return item + " - " + location;
    }

    static string CreateLabel(string item, string location, bool fragile)
    {
        string marker = fragile ? "FRAGILE" : "standard";
        return item + " - " + location + " - " + marker;
    }
}

Output:

Notebook
Backpack - Aisle 3
Laptop - Secure Cabinet - FRAGILE

This overload set grows the same idea from a simple label to a more detailed label. The compiler can choose by argument count before it even considers type conversions.

Realistic Numeric Overloads

using System;
using System.Globalization;

class Program
{
    static void Main()
    {
        decimal price = 100.00m;

        Console.WriteLine(ApplyDiscount(price, 15));
        Console.WriteLine(ApplyDiscount(price, 0.125m));
    }

    static string ApplyDiscount(decimal price, int percentOff)
    {
        decimal rate = percentOff / 100m;
        decimal discounted = price * (1m - rate);
        return "percent discount: " + discounted.ToString("0.00", CultureInfo.InvariantCulture);
    }

    static string ApplyDiscount(decimal price, decimal rate)
    {
        decimal discounted = price * (1m - rate);
        return "rate discount: " + discounted.ToString("0.00", CultureInfo.InvariantCulture);
    }
}

Output:

percent discount: 85.00
rate discount: 87.50

The overloads communicate two different ways to express a discount: an integer percent such as 15, or a decimal rate such as 0.125m. This is useful only because the two meanings are clear from the parameter types and the method documentation.

How Overload Resolution Works Step by Step

  1. The compiler finds all visible methods with the requested name.
  2. It keeps only candidates whose parameter count can work, including optional parameters and params where applicable.
  3. It checks whether each argument can be converted to the corresponding parameter type.
  4. It ranks conversions. An exact type match is better than a numeric conversion, and a numeric conversion is usually better than boxing to object.
  5. If one candidate is better than all others, the compiler binds the call to that overload and emits a call to that method signature.
  6. If no candidate is best, the call is rejected as ambiguous.

Overload resolution is why Print(5) chooses Print(int) over Print(double) when both exist: int is an exact match, while double would require a conversion. It is also why null can be tricky. The literal null can convert to many reference types, so a call may need a cast to say which overload you mean.

Common Mistakes

Trying to Overload by Return Type Only

static int ConvertText(string text)
{
    return int.Parse(text);
}

static double ConvertText(string text)
{
    return double.Parse(text);
}

This does not compile because both methods have the same name and the same parameter list. The caller might write ConvertText("42"), and the compiler cannot use the assignment target reliably enough to make return type the overload key. Use different names or add a meaningful parameter:

using System;
using System.Globalization;

class Program
{
    static void Main()
    {
        Console.WriteLine(ToInt32("42"));
        Console.WriteLine(ToDouble("42.5").ToString("0.0", CultureInfo.InvariantCulture));
    }

    static int ToInt32(string text)
    {
        return int.Parse(text, CultureInfo.InvariantCulture);
    }

    static double ToDouble(string text)
    {
        return double.Parse(text, CultureInfo.InvariantCulture);
    }
}

Output:

42
42.5

Creating an Ambiguous null Call

static void Show(string text)
{
    Console.WriteLine("text");
}

static void Show(int[] numbers)
{
    Console.WriteLine("numbers");
}

Show(null);

The literal null can convert to string and to int[]. Neither overload is more specific for this call, so the compiler rejects it. Cast null when you need a specific overload:

using System;

class Program
{
    static void Main()
    {
        Show((string?)null);
        Show((int[]?)null);
    }

    static void Show(string? text)
    {
        Console.WriteLine(text is null ? "text is null" : text);
    }

    static void Show(int[]? numbers)
    {
        Console.WriteLine(numbers is null ? "numbers are null" : numbers.Length.ToString());
    }
}

Output:

text is null
numbers are null

Mixing Optional Parameters with Similar Overloads

using System;

class Program
{
    static void Main()
    {
        Save("report.txt");
        Save("report.txt", overwrite: true);
    }

    static void Save(string fileName)
    {
        Console.WriteLine("basic save: " + fileName);
    }

    static void Save(string fileName, bool overwrite = false)
    {
        Console.WriteLine("overwrite save: " + fileName + ", " + overwrite);
    }
}

Output:

basic save: report.txt
overwrite save: report.txt, True

This compiles, but it can surprise readers. The first call chooses the one-parameter overload, not the two-parameter overload with its default value. Optional parameters and overloads both make calls flexible; using too many of them together can make call sites harder to predict.

Best Practices

  • Overload only when each method represents the same operation with different inputs.
  • Prefer overloads that differ clearly by argument count or by strongly different types.
  • Do not overload by return type only; C# does not allow it.
  • Avoid overload sets that differ only by several parameters of the same primitive type, such as many string or bool combinations.
  • Use named methods when the behavior is meaningfully different, such as Parse versus TryParse.
  • Be cautious when combining overloads with optional parameters, because omitted arguments can make intent less obvious.
  • Cast null or use typed variables when a reference-type overload would otherwise be ambiguous.
  • Keep overload behavior consistent. If one overload validates, trims, rounds, or throws in a certain way, related overloads should usually follow the same rule.

Practice Exercises

  1. Write three overloaded Area methods: one for a square side length, one for rectangle width and height, and one for a circle radius.
  2. Create overloaded PrintBadge methods for just a name, a name plus role, and a name plus role plus employee id.
  3. Write two Find overloads: one that searches an int[] for a number and one that searches a string[] for text. Return the matching index or -1.

Summary

  • Method overloading lets several methods share a name when their parameter lists differ.
  • The return type is not enough to create a different overload.
  • The compiler chooses an overload by comparing argument count, argument types, conversions, optional parameters, and modifiers.
  • Overload resolution happens at compile time and emits a call to a specific method signature.
  • Ambiguous calls, especially with null or similar numeric conversions, must be clarified.
  • Good overloads make an API easier to use; confusing overloads hide important differences behind the same name.