Interpolated strings: advanced usages
This blog post will show you how to take advantage of the interpolated strings to do more than just a basic string concatenation. Indeed, interpolated strings are often used as an easier way to concatenate strings. For instance:
var fullname = "Gérald Barré";
var nickname = "Meziantou";
var str = fullname + " aka. " + nickname;
var str = string.Format("{0} aka. {1}", fullname, nickname);
var str = $"{fullname} aka. {nickname}"; // Interpolated string is more readable
As with string.Format
, you can use a custom format using a colon to separate the value and the format:
var value = 42;
Console.WriteLine($"{value:C}"); // $42.00
It is possible to use multiline interpolated strings using the prefix $@
or @$
:
var publishDate = new DateTime(2017, 12, 14);
var str = $@"This post published on {publishDate:yyyy-MM-dd} is about
interpolated strings.";
#Under the hood
The compiler has 4 different ways to convert an interpolated string. Depending on the usage, it will automatically choose the most performant one.
##Interpolated strings rewritten as string.Concat
If the interpolated string is assigned to a string variable/parameter and all arguments are of type String, the compiler will rewrite the interpolated string to string.Concat
.
string name = "meziantou";
string hello = $"Hello {name}!";
The previous code is rewritten by the compiler as:
string name = "meziantou";
string hello = string.Concat("Hello ", name, "!");
##Interpolated strings rewritten as string.Format
If the interpolated string is assigned to a string variable/parameter and some of the arguments are not of type string, the compiler will rewrite the interpolated string to string.Format
.
DateTime now = DateTime.Now;
string str = $"It is {now}";
The previous code is rewritten by the compiler as:
DateTime now = DateTime.Now;
string str = string.Format("It is {0}", now);
##Interpolated strings rewritten as FormattableString
If the interpolated string is assigned to a FormattableString
variable/parameter, the compiler rewrites the interpolated string to create a new FormattableString
using FormattableStringFactory.Create
:
object value1 = "Foo";
object value2 = "Bar";
FormattableString str = $"Test {value1} {value2}";
The previous code is rewritten by the compiler as:
object value1 = "Foo";
object value2 = "Bar";
FormattableString str = FormattableStringFactory.Create("Test {0} {1}", new object[] { value1, value2 });
The factory creates an instance of ConcreteFormattableString
using the format and the arguments. Here's the code of the class:
class ConcreteFormattableString : FormattableString
{
private readonly string _format;
private readonly object[] _arguments;
internal ConcreteFormattableString(string format, object[] arguments)
{
_format = format;
_arguments = arguments;
}
public override string Format => _format;
public override object[] GetArguments() => _arguments;
public override int ArgumentCount => _arguments.Length;
public override object GetArgument(int index) => _arguments[index];
public override string ToString(IFormatProvider formatProvider)
{
return string.Format(formatProvider, _format, _arguments);
}
}
The full code source of the factory is available on GitHub in the CoreCLR repo: FormattableStringFactory.cs, FormattableString.cs.
In the end, the ToString
method will call string.Format
with the arguments and the specified FormatProvider
.
##Interpolated strings rewritten as constants (C# 10)
Starting with C# 10, the compiler rewrites string interpolations as constant strings when all interpolated values are constants.
const string Username = "meziantou";
const string Hello = $"Hello {Username}!";
// In previous C# version, you need to use the following concat syntax
const string Hello2 = "Hello " + Username + "!";
This is useful when using the string in locations where a constant is expected such as an attribute value.
// Works with C# 10
[DebuggerDisplay($"Value: {nameof(Text)}")]
public class Sample
{
public string Text { get; set; }
}
##Interpolated string handlers (C# 10)
C# 10 and .NET 6 introduces a new feature named "interpolated string handlers". This change adds a new way to lower interpolated strings when performance is needed. Let's see how Debug.Assert
use this feature to prevent generating a string when the condition is met. Instead of a string
or FormattableString
argument, it uses a custom type an use the InterpolatedStringHandlerArgument
attribute to indicate to the compiler that it must rewrite interpolated strings.
public static void Assert(
[DoesNotReturnIf(false)] bool condition,
[InterpolatedStringHandlerArgument("condition")] AssertInterpolatedStringHandler message)
{
Assert(condition, message.ToString());
}
When you call the Assert
method, the compiler will rewrite the interpolated string to construct a new AssertInterpolatedStringHandler
instance:
Debug.Assert(condition: false, $"Debug info: {ComputeDebugInfo()}");)
var handler = new AssertInterpolatedStringHandler(
literalLength: 12,
formattedCount: 1,
condition: false,
out var shouldAppend);
if (shouldAppend)
{
handler.AppendLiteral("Debug info: ");
handler.AppendFormatted(ComputeDebugInfo());
}
Debug.Assert(condition, handler);
You can read more about this new feature in the following post: String Interpolation in C# 10 and .NET 6
#Specifying culture
Using the old String.Format
, you can specify the culture to use to format the values:
var culture = CultureInfo.GetCultureInfo("fr-FR");
string.Format(culture, "{0:C}", 42); // 42,00 €
The interpolated string syntax doesn't provide a way to directly set the format provider. By default, it uses the current culture (source). You can also use the invariant culture by using the FormattableString.Invariant
method:
var value = 42;
Console.WriteLine(FormattableString.Invariant($"Value {value:C}")); // Value ¤42.00
// You can simplify the usage of Invariant with "using static"
using static System.FormattableString;
Console.WriteLine(Invariant($"Value {value:C}")); // Value ¤42.00
If you want to use a specific culture, you'll have to implement your own method (very simple):
private static string WithCulture(CultureInfo cultureInfo, FormattableString formattableString)
{
return formattableString.ToString(cultureInfo);
}
WithCulture(CultureInfo.GetCultureInfo("jp-JP"), $"{value:C}"); // ¥42.00
WithCulture(CultureInfo.GetCultureInfo("fr-FR"), $"{value:C}"); // 42,00 €
That's the basics. Now, let's use the FormattableString
class to do some trickier things 😃
Starting with .NET 6 and C# 10, you can use the more performant method string.Create
to format a string using a culture
string.Create(CultureInfo.InvariantCulture, $"Value {value:C}");
#Escaping command-line arguments
The first example consists of escaping the values to use them as command-line arguments. The final result looks like:
var arg1 = "c:\\Program Files\\whoami.exe";
var arg2 = "Gérald Barré";
var commandLine = EscapeCommandLineArgs($"{arg1} {arg2}"); // "c:\Program Files\whoami.exe" "Gérald Barré"
First, install the NuGet package Meziantou.Framework.CommandLine
(NuGet, GitHub), a package for building command lines. It follows the rules provided in the very interesting post: Everyone quotes command line arguments the wrong way.
Now, you can escape the arguments of the command line using the CommandLine
class, and then call string.Format
:
string EscapeCommandLineArgs(FormattableString formattableString)
{
var args = formattableString.GetArguments()
.Select(arg => CommandLineBuilder.WindowsQuotedArgument(string.Format("{0}", arg)))
.ToArray();
return string.Format(formattableString.Format, args);
}
This works great in most cases. But it doesn't respect the format of the arguments. For instance, if the string is $"{0:C}"
, the C
format is forgotten. A better way is to create a custom class that implements IFormatProvider
. The Format
method of the formatter is called once for each argument with their value and format. This, you can process them and output the value with the actual format. The code is a little longer, but it respects the formatting of the arguments:
string EscapeCommandLineArgs(FormattableString formattableString)
{
return formattableString.ToString(new CommandLineFormatProvider());
}
class CommandLineFormatProvider : IFormatProvider
{
public object GetFormat(Type formatType)
{
if (typeof(ICustomFormatter).IsAssignableFrom(formatType))
return new CommandLineFormatter();
return null;
}
private class CommandLineFormatter : ICustomFormatter
{
public string Format(string format, object arg, IFormatProvider formatProvider)
{
if (arg == null)
return string.Empty;
if (arg is string str)
return CommandLineBuilder.WindowsQuotedArgument(str);
if (arg is IFormattable) // Format the argument before escaping the value
return CommandLineBuilder.WindowsQuotedArgument(((IFormattable)arg).ToString(format, CultureInfo.InvariantCulture));
return CommandLineBuilder.WindowsQuotedArgument(arg.ToString());
}
}
}
#Executing a SQL query with parameters
Now, let's see how to use interpolated strings to create a parameterized query. Using parameterized queries is important for security, as it's a way to protect from SQL injection attacks, and for performance.
The idea is to replace arguments by @p0
, @p1
and so on to create the SQL query. Then, you can create command parameters with the actual values.
using (var sqlConnection = new SqlConnection())
{
sqlConnection.Open();
ExecuteNonQuery(sqlConnection, $@"
UPDATE Customers
SET Name = {"Meziantou"}
WHERE Id = {1}");
}
void ExecuteNonQuery(DbConnection connection, FormattableString formattableString)
{
using (var command = connection.CreateCommand())
{
// Replace values by @p0, @p1, @p2, ....
var args = Enumerable.Range(0, formattableString.ArgumentCount).Select(i => (object)("@p" + i)).ToArray();
command.CommandType = System.Data.CommandType.Text;
command.CommandText = string.Format(formattableString.Format, args);
// Create parameters
for (var i = 0; i < formattableString.ArgumentCount; i++)
{
var arg = formattableString.GetArgument(i);
var p = command.CreateParameter();
p.ParameterName = "@p" + i;
p.Value = arg;
command.Parameters.Add(p);
}
// Execute the command
command.ExecuteNonQuery();
}
}
#Do not use overloads with string and FormattableString
If the compiler has the choice, it will choose the string overload even if you use an interpolated string as an argument.
// Do not do that
void Sample(string value) => throw null;
void Sample(FormattableString value) => throw null;
Sample($"Hello {name}");
// ⚠ Call Sample(string)
Sample((FormattableString)$"Hello {name}");
// Call Sample(FormattableString) because of the explicit cast
Sample((FormattableString)$"Hello {name}" + "!");
// ⚠ Call Sample(string) because the operator FormattableString + string returns a string
This is not convenient and very error-prone. This is why you should avoid having overloads for both FormattableString
and regular string.
#Conclusion
Interpolated strings are a very nice feature introduced in C# 6. It allows us to use the functionalities of string.Format
but with a much nicer syntax. Taking advantage of the format provider allows writing more readable code in some scenarios. For instance, you can automatically escape values, or do something different from concatenating strings such as executing a SQL query.
Do you have a question or a suggestion about this post? Contact me!