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.
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").
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 parameters | Overloads | |
|---|---|---|
| How many methods to write | One | One per combination |
| Default visible to the caller | Yes, in the signature and in tooltips | No — it lives inside the shorter overload |
| Changing a default later | Callers must be recompiled to see it | Change one body and every caller gets it |
| Different behaviour per combination | Awkward — one body handles every case | Natural, each overload can differ |
| A public library API | Convenient, but a versioning commitment | Safer 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.
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.