Optional and Named Arguments

A parameter with a default value may be left out by the caller, and any argument can be labelled with the parameter's name instead of its position.

SyntaxreturnType MethodName(type required, type optional = defaultValue) // call: MethodName(value, optional: other)

Some parameters have a sensible value almost every time. Giving such a parameter a default value makes it optional: the caller may leave it out, and the default is used. This replaces writing several near-identical methods that differ only in how many values they take.

Writing a default

Write = value after the parameter. The value must be a compile-time constant, or new ValType() or default(ValType) for a value type. All optional parameters must come after all required ones.

Named arguments

At the call you can write parameterName: value instead of relying on position. This does two things. It lets you skip over an optional parameter you do not care about, because gaps such as M(3, , 4) are not allowed. It also makes a call with several same-typed arguments readable. Since C# 7.2 a named argument may sit in a non-trailing position, but a positional argument may not follow a named one that is out of order; that is error CS1738.

Where the default actually lives

The compiler does not look the default up at run time. It copies the default value into the calling code. The call Line("name") is compiled as Line("name", '.', 10). So a library that changes a default value changes nothing for callers already compiled, until those callers are rebuilt.

What trips people up

Because defaults are baked into callers, a widely used public API should treat a default value as part of its contract. Prefer separate overloads when the value must be able to change later.

Example

Example · csharp
// pad and width are optional: they have default values.
// Optional parameters must come after every required one.
string Line(string text, char pad = '.', int width = 10)
{
    // PadRight(int totalWidth, char paddingChar)
    return text.PadRight(width, pad);
}

Console.WriteLine(Line("name"));                 // both defaults used
Console.WriteLine(Line("name", '-'));            // pad given, width defaulted

// A named argument skips over pad without naming a value for it.
Console.WriteLine(Line("name", width: 6));

// When every argument is named, the order is free.
Console.WriteLine(Line(width: 6, text: "age"));

// Output:
// name......
// name------
// name..
// age...

When to use it

  • Add a configurable timeout to an existing method without breaking the callers that already exist.
  • Call a method that takes five optional settings while specifying only the one you care about.
  • Make a call site with three bool arguments readable without changing the method.

More examples

The gap that is not allowed

This is the everyday reason to use a named argument: you want the last optional parameter but not the ones before it. Commas with nothing between them are never valid C#.

Example · csharp
void Connect(string host, int port = 5432, int timeoutSeconds = 30)
{
    Console.WriteLine($"{host}:{port} timeout {timeoutSeconds}s");
}

Connect("db.internal");

// You cannot leave a hole in the argument list to reach the third parameter.
// Connect("db.internal", , 5);   // error

// Name the one you want instead.
Connect("db.internal", timeoutSeconds: 5);

// Output:
// db.internal:5432 timeout 30s
// db.internal:5432 timeout 5s

What may and may not be a default

Defaults are limited to constant expressions, new ValType() and default(ValType). For a collection or any other object, default the parameter to null and build the real value inside the method.

Example · csharp
// Constants are fine. A nullable reference type plus null is the usual way
// to say "no value supplied" for a string or an object.
string Label(string name, string? suffix = null, int version = 1)
{
    return suffix is null ? $"{name} v{version}" : $"{name}-{suffix} v{version}";
}

// This would not compile: a new list is not a constant expression.
// void Bad(List<string> items = new List<string>()) { }

Console.WriteLine(Label("report"));
Console.WriteLine(Label("report", "draft", version: 2));

// Output:
// report v1
// report-draft v2

Named arguments for readability

Named arguments cost nothing at run time and remove a whole class of silent mistakes when several parameters share a type. Use them whenever a call reads as a row of true and false.

Example · csharp
// Three bools in a row tell a reader nothing at the call site.
string Render(string body, bool wrap, bool trim, bool upper)
{
    string text = trim ? body.Trim() : body;
    text = upper ? text.ToUpperInvariant() : text;
    return wrap ? $"[{text}]" : text;
}

// Positional: correct, but unreadable.
Console.WriteLine(Render("  hi  ", true, true, false));

// Named: the same call, and it explains itself.
Console.WriteLine(Render("  hi  ", wrap: true, trim: true, upper: false));

// Output:
// [hi]
// [hi]

Discussion

  • Be the first to comment on this lesson.