String Interpolation

Build text from values with $"...", the shorthand for concatenation and string.Format, including width and format specifiers.

Syntax$"text {expression[,width][:formatString]} text"

String interpolation builds a string out of fixed text and values. Put $ immediately before the opening quote, then write an expression inside braces. The compiler replaces each brace pair with the text form of that value. It was added in C# 6.

The longhand it replaces

These three lines do the same job:

string a = "hi " + name + ", you have " + count + " items";
string b = string.Format("hi {0}, you have {1} items", name, count);
string c = $"hi {name}, you have {count} items";

In string.Format the {0} and {1} are positions in the argument list that follows, so the braces and the arguments have to be kept in step by hand. Interpolation writes the expression where the placeholder is, so there is no list to get wrong. The compiler normally turns an interpolated string into a string.Concat or string.Format call. It is the same work, written more clearly.

Width and format

A placeholder has three parts: {expression[,width][:formatString]}. The parts in brackets are optional. width is the minimum number of characters to use; a positive number pads on the left (right-aligned) and a negative number pads on the right (left-aligned). formatString is whatever the value's own type understands, such as F2 for two decimal places or D5 for an integer padded to five digits.

What trips people up

A colon inside the braces starts the format string, so a conditional expression has to be wrapped in parentheses. To print a literal brace, double it: {{ prints {.

Example

Example · csharp
using System.Globalization;

// Pinned so the formatted number below is identical on every machine.
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

string name = "ada";
int count = 3;
decimal price = 12.5m;

// Longhand 1: glue the pieces together with +
string withPlus = "cart: " + name + " has " + count + " items";

// Longhand 2: string.Format, where {0} and {1} are positions in the list after it
string withFormat = string.Format("cart: {0} has {1} items", name, count);

// Shorthand: interpolation. The compiler produces the same work as the two above.
string withInterpolation = $"cart: {name} has {count} items";

Console.WriteLine(withPlus);
Console.WriteLine(withFormat);
Console.WriteLine(withInterpolation);

// {expression,width:format}. -8 pads on the right, 8 pads on the left.
Console.WriteLine($"|{name,-8}|{price,8:F2}|");

// Two braces print one brace.
Console.WriteLine($"{{{count}}}");

// Output:
// cart: ada has 3 items
// cart: ada has 3 items
// cart: ada has 3 items
// |ada     |   12.50|
// {3}

When to use it

  • Write a log line that names the user and the record it failed on.
  • Format a money amount to two decimal places for an invoice line.
  • Build an error message that includes the value that failed validation.
  • Lay out a fixed-width console report with aligned columns.

More examples

A colon means format, so wrap conditionals in parentheses

This is the error people hit first: without the parentheses the compiler reads everything after the colon as a format string and reports a confusing error about the format. The same braces accept any expression, which is why interpolation replaces most small helper methods that existed only to assemble a message.

Example · csharp
int age = 1;
int years = 34;

// The colon in a conditional expression would be read as the start of a
// format string, so the whole expression needs parentheses.
Console.WriteLine($"{age} year{(age == 1 ? "" : "s")} old");
Console.WriteLine($"{years} year{(years == 1 ? "" : "s")} old");

// Any expression is allowed inside the braces, not just a variable.
Console.WriteLine($"next year: {years + 1}");
Console.WriteLine($"upper: {"ada".ToUpperInvariant()}");

// Output:
// 1 year old
// 34 years old
// next year: 35
// upper: ADA

Width to line up a column of output

Width is how you produce aligned console output or a fixed-width report without counting spaces by hand. Width is a minimum, not a maximum: a value longer than the width is printed in full and the column shifts, so pick a width that fits your largest expected value.

Example · csharp
using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

// Printed left to right: name left-aligned in 10, amount right-aligned in 9.
Console.WriteLine($"{"item",-10}{"amount",9}");
Console.WriteLine($"{"widget",-10}{4.5m,9:F2}");
Console.WriteLine($"{"bolt",-10}{120m,9:F2}");
Console.WriteLine($"{"total",-10}{124.5m,9:F2}");

// Output:
// item         amount
// widget         4.50
// bolt         120.00
// total        124.50

Pinning the culture for machine-readable text

This is the bug behind a surprising number of broken CSV exports and rejected API payloads: the same code produces different text on a developer's machine and on a server with different regional settings. Use the current culture for anything a person reads, and string.Create with InvariantCulture for anything another program parses.

Example · csharp
using System.Globalization;

// Pretend this process runs in a locale that uses a comma for decimals.
CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("de-DE");

decimal amount = 1234.5m;

// Follows the current culture: fine for a screen, wrong for a data file.
string forPeople = $"{amount:N2}";

// string.Create takes the interpolated string and a culture to format it with.
// Available since .NET 6.
string forMachines = string.Create(CultureInfo.InvariantCulture, $"{amount:N2}");

Console.WriteLine(forPeople);
Console.WriteLine(forMachines);

// Output:
// 1.234,50
// 1,234.50

Discussion

  • Be the first to comment on this lesson.