Overloading and Local Functions
Overloading lets several methods share one name as long as their parameters differ, so callers can use the natural name whatever they pass in. Local functions are methods declared inside other methods, keeping small helpers right next to the only code that uses them.
Method overloading
Overloads have the same name but different parameter lists — a different number of parameters, or different types. The compiler picks the one that best matches the arguments. Overloads must be members of a class, so these live in a small static class:
Console.WriteLine(Info.Describe(42)); Console.WriteLine(Info.Describe(3.14)); Console.WriteLine(Info.Describe("hello")); Console.WriteLine(Info.Describe(2, 3)); static class Info { public static string Describe(int n) => $"int {n}"; public static string Describe(double d) => $"double {d}"; public static string Describe(string s) => $"string of length {s.Length}"; public static string Describe(int a, int b) => $"two ints, sum {a + b}"; }
int 42 double 3.14 string of length 5 two ints, sum 5
You've already used overloads: Console.WriteLine has versions for int, double, string, bool and more, and Math.Round has versions with and without a number of decimals.
What counts as different
Only the parameter list matters. Two methods that differ only in their return type or parameter names conflict:
static class Parser { public static int Parse(string text) => int.Parse(text); public static double Parse(string text) => double.Parse(text); }
Program.cs(4,26): error CS0111: Type 'Parser' already defines a member called 'Parse' with the same parameter types
Methods written directly after top-level statements are local functions (more on those below), and local functions can't be overloaded at all — two with the same name are always an error. That's why the overloading examples in this lesson put their methods inside a class.
Overload resolution
When several overloads could work, the compiler chooses the most specific one, preferring exact matches over conversions:
Printer.Show(5); // exact match: int Printer.Show(5L); // long Printer.Show(5.0f); // float has no overload: converted to double Printer.Show('A'); // char converts to int static class Printer { public static void Show(int x) => Console.WriteLine($"int: {x}"); public static void Show(long x) => Console.WriteLine($"long: {x}"); public static void Show(double x) => Console.WriteLine($"double: {x}"); }
int: 5 long: 5 double: 5 int: 65
If two overloads are equally good, the call is ambiguous and won't compile:
Shipping.Cost(10, 10); static class Shipping { public static double Cost(int weight, double distance) => weight * distance; public static double Cost(double weight, int distance) => weight * distance; }
Program.cs(1,10): error CS0121: The call is ambiguous between the following methods or properties: 'Shipping.Cost(int, double)' and 'Shipping.Cost(double, int)'
Overloads or optional parameters?
Both let callers supply fewer arguments. Optional parameters are simpler when the method body is the same and only a default changes. Overloads are better when the inputs are genuinely different types or need different logic:
Console.WriteLine(Area.Of(3)); // circle Console.WriteLine(Area.Of(3, 4)); // rectangle Console.WriteLine(Area.Of([0, 0], [4, 0], [0, 3])); // triangle from points static class Area { public static double Of(double radius) => Math.Round(Math.PI * radius * radius, 2); public static double Of(double width, double height) => width * height; public static double Of(double[] a, double[] b, double[] c) => Math.Abs((b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1])) / 2; }
28.27 12 6
Local functions
A local function is declared inside another method and can only be called from there. It's ideal for a helper that only makes sense in one place. (The methods you've written after top-level statements are, in fact, local functions of the hidden Main.)
Console.WriteLine(Report.Build([3, 8, 12, 5])); static class Report { public static string Build(int[] values) { return $"min {Format(values.Min())}, max {Format(values.Max())}"; static string Format(int n) => n < 10 ? $"0{n}" : n.ToString(); } }
min 03, max 12
Local functions can be declared after the return statement, keeping the main logic at the top.
Capturing variables
A non-static local function can use the variables of the method around it. Marking it static forbids that, which documents that it's self-contained and avoids accidental dependencies:
int threshold = 50; int[] readings = [12, 75, 49, 88, 50]; foreach (int r in readings) { if (IsHigh(r)) Console.WriteLine($"{r} is above {threshold}"); } bool IsHigh(int value) => value > threshold; // captures 'threshold'
75 is above 50 88 is above 50
int threshold = 50; Console.WriteLine(IsHigh(60)); static bool IsHigh(int value) => value > threshold;
Program.cs(4,42): error CS8421: A static local function cannot contain a reference to 'threshold'.
Local functions for validation
A common use is separating argument checks from the main work, with the helper doing the real computation:
Console.WriteLine(MathUtil.Power(2, 10)); Console.WriteLine(MathUtil.Power(3, 0)); static class MathUtil { public static long Power(long baseValue, int exponent) { ArgumentOutOfRangeException.ThrowIfNegative(exponent); return Compute(baseValue, exponent); static long Compute(long b, int e) { long result = 1; for (int i = 0; i < e; i++) result *= b; return result; } } }
1024 1
ArgumentOutOfRangeException.ThrowIfNegative is one of several built-in guard helpers; lesson 52 shows more.
Try it yourself
Add an overload of Repeat that takes a char and a separator string.
ababab *****