Skip to main content
ANVISoftware Solutions
Lesson 19 of 62Beginner13 min

Optional and Named Arguments

By the end of this lesson

Provide defaults and call methods readably when several parameters share a type.

Two features that work as a pair. Default values let a caller leave out what they do not care about. Named arguments let a call say what each value means.

Together they solve a specific problem: a method with four parameters, three of which are usually the same, called from code where the numbers alone tell you nothing.

One method, three ways to call it
C#
Console.WriteLine(FormatInvoiceLine("Desk lamp", 24.99m));
Console.WriteLine(FormatInvoiceLine("Desk lamp", 24.99m, quantity: 3));
Console.WriteLine(FormatInvoiceLine("Desk lamp", 24.99m, currencyCode: "EUR"));

static string FormatInvoiceLine(
    string description,
    decimal unitPrice,
    int quantity = 1,
    string currencyCode = "GBP")
{
    decimal lineTotal = unitPrice * quantity;

    return $"{description} x{quantity} = {currencyCode} {lineTotal:N2}";
}
  • A parameter with = after it is optional. Leave it out and the default is used.
  • Optional parameters have to come after every required one, because the compiler matches arguments by position before it looks at names.
  • quantity: 3 is a named argument. The third call uses one to skip quantity entirely and set only the currency — which positional arguments cannot do.
  • The names also carry meaning at the call site. Without them, the third call would read FormatInvoiceLine("Desk lamp", 24.99m, 1, "EUR").
A default the compiler cannot evaluate
C#
List<(string Reference, DateTime DueDate)> invoices = new List<(string, DateTime)>
{
    ("INV-2041", new DateTime(2026, 2, 28)),
    ("INV-2042", new DateTime(2026, 4, 30)),
};

var overdueNow = FindOverdue(invoices);
var overdueAtMonthEnd = FindOverdue(invoices, asAt: new DateTime(2026, 3, 31), graceDays: 5);

Console.WriteLine($"{overdueNow.Count} now, {overdueAtMonthEnd.Count} at month end");

static List<string> FindOverdue(
    List<(string Reference, DateTime DueDate)> invoices,
    DateTime? asAt = null,
    int graceDays = 0)
{
    DateTime effectiveDate = asAt ?? DateTime.UtcNow;
    List<string> overdue = new List<string>();

    foreach (var invoice in invoices)
    {
        if (invoice.DueDate.AddDays(graceDays) < effectiveDate)
        {
            overdue.Add(invoice.Reference);
        }
    }

    return overdue;
}
  • DateTime.UtcNow cannot be a default value, because a default has to be something the compiler can write down. A nullable parameter defaulting to null, then ?? inside the method, is the usual way round it.
  • graceDays = 0 is a genuine default: no grace period is an honest starting point that cannot mislead.
  • The second call names both arguments. With a date and a number sitting next to each other, the names are what stop a future edit swapping them.

What a default value is allowed to be:

  • A literal: 1, "GBP", true, 0m
  • A const, or an enum member
  • The word default, which gives 0, false or null depending on the type
  • new() for a struct with no constructor arguments
  • Not a method call, not DateTime.Now, and not a new list — for those, default to null and substitute inside the method
  • Named arguments normally follow the positional ones; they may come earlier only when they sit in their own position, which is rarely worth the confusion

Optional parameters or overloads:

 Optional parametersOverloads
How many methods to writeOneOne per combination
Default visible to the callerYes, in the signature and in tooltipsNo — it lives inside the shorter overload
Changing a default laterCallers must be recompiled to see itChange one body and every caller gets it
Different behaviour per combinationAwkward — one body handles every caseNatural, each overload can differ
A public library APIConvenient, but a versioning commitmentSafer to evolve over time

Summary

  • Optional parameters must follow the required ones, and their defaults must be compile-time values
  • Named arguments let a caller skip an optional parameter and explain what each value means
  • Defaults are copied into the call site, so changing one needs callers to be rebuilt
  • Add new optional parameters at the end; inserting one changes the meaning of positional calls
  • For a public API, an extra overload is easier to evolve than a new default

Practice

Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.

Try it yourself

Try it yourself

Write a method that raises an invoice: a required customer id, then a payment term in days defaulting to 30, a currency code defaulting to GBP, and a flag for copying the accounts team that defaults to false.

Call it three ways: the plain case, a 7-day invoice in euros, and one that copies accounts. Use named arguments wherever a bare value would not explain itself.

Show solution

The bool is the argument that most needs a name. RaiseInvoice(1041, 7, "EUR", true) has one value a reader cannot decode, and it is the one that changes who receives an email.

The 7-day euro call could be positional, since a number and a currency code are hard to confuse. Naming them still helps if a fifth parameter arrives later.

Note what the defaults say about the domain: 30 days and GBP are the normal case, so the common call stays short and the unusual ones are visibly unusual.

C#
Console.WriteLine(RaiseInvoice(1041));
Console.WriteLine(RaiseInvoice(1041, paymentTermDays: 7, currencyCode: "EUR"));
Console.WriteLine(RaiseInvoice(1041, copyAccounts: true));

static string RaiseInvoice(
    int customerId,
    int paymentTermDays = 30,
    string currencyCode = "GBP",
    bool copyAccounts = false)
{
    string copyNote = copyAccounts ? ", copied to accounts" : "";

    return $"Invoice for customer {customerId}: {currencyCode}, " +
           $"due in {paymentTermDays} days{copyNote}";
}

Think about it

Think about it

C# refuses to let DateTime.UtcNow be a default parameter value, even though it is exactly the default a lot of methods would want.

Why is that restriction there, and what would be ambiguous if it were allowed?

Show solution

A default value is written into the calling code at compile time. There is nowhere to put a value that will not exist until the method is called.

If the language allowed it, the obvious question would be when the expression runs: once when the method is compiled, once per call, or once per program. Every answer surprises somebody, and the first would freeze the timestamp forever.

The nullable-parameter pattern makes the timing explicit instead. asAt ?? DateTime.UtcNow evaluates on each call, in the method, where a reader can see it. It also lets a test pass a fixed date, which a built-in default would not.

Saved in this browser only.