{"id":33717,"date":"2021-08-10T15:27:31","date_gmt":"2021-08-10T22:27:31","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/dotnet\/?p=33717"},"modified":"2021-09-15T12:40:13","modified_gmt":"2021-09-15T19:40:13","slug":"string-interpolation-in-c-10-and-net-6","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/dotnet\/string-interpolation-in-c-10-and-net-6\/","title":{"rendered":"String Interpolation in C# 10 and .NET 6"},"content":{"rendered":"<p>Text processing is at the heart of huge numbers of apps and services, and in .NET, that means lots and lots of <code>System.String<\/code>. <code>String<\/code> creation is so fundamental that a myriad of ways of creating them have existed since .NET Framework 1.0 was released, and more have joined the fray since. Whether via <code>String<\/code>&#8216;s constructors, or <code>StringBuilder<\/code>, or <code>ToString<\/code> overrides, or helper methods on <code>String<\/code> like <code>Join<\/code> or <code>Concat<\/code> or <code>Create<\/code> or <code>Replace<\/code>, APIs to create strings are ubiquitous. One of the most powerful APIs for creating strings in .NET, however, is <code>String.Format<\/code>.<\/p>\n<p>The <code>String.Format<\/code> method has a multitude of overloads, all of which share in common the ability to supply a &#8220;composite format string&#8221; and associated arguments. That format string contains a mixture of literal text and placeholders, sometimes referred to as &#8220;format items&#8221; or &#8220;holes&#8221;, which are then filled in with the supplied arguments by the formatting operation. For example, <code>string.Format(\"Hello, {0}! How are you on this fine {1}?\", name, DateTime.Now.DayOfWeek)<\/code>, given a name of <code>\"Stephen\"<\/code> and invoked on a Thursday, will output a string <code>\"Hello, Stephen! How are you on this fine Thursday?\"<\/code>. Additional functionality is available, such as the ability to provide a format specifier, e.g. <code>string.Format(\"{0} in hex is 0x{0:X}\", 12345)<\/code> will produce the string <code>\"12345 in hex is 0x3039\"<\/code>.<\/p>\n<p>These capabilities all result in <code>String.Format<\/code> being a workhorse that powers a significant percentage of string creation. In fact, it&#8217;s so important and useful, C# language syntax was added in C# 6 to make it even more usable. This &#8220;string interpolation&#8221; functionality enables developers to place a <code>$<\/code> character just before the string; then, rather than specifying arguments for the format items separately, those arguments can be embedded directly into the interpolated string. For example, my earlier &#8220;Hello&#8221; example can now be written as <code>$\"Hello, {name}! How are you on this fine {DateTime.Now.DayOfWeek}?\"<\/code>, which will produce exactly the same string but via a more convenient syntax.<\/p>\n<p>The C# compiler is free to generate whatever code it deems best for an interpolated string, as long as it ends up producing the same result, and today it has multiple mechanisms it might employ, depending on the situation. If, for example, you were to write:<\/p>\n<pre><code class=\"language-C#\">const string Greeting = \"Hello\";\r\nconst string Name = \"Stephen\";\r\nstring result = $\"{Greeting}, {Name}!\";<\/code><\/pre>\n<p>the C# compiler can see that all portions of the interpolated string are string literals, and it can emit this into IL as if it had been written as a single string literal:<\/p>\n<pre><code class=\"language-C#\">string result = \"Hello, Stephen!\";<\/code><\/pre>\n<p>Or, for example, if you were to write:<\/p>\n<pre><code class=\"language-C#\">public static string Greet(string greeting, string name) =&gt; $\"{greeting}, {name}!\";<\/code><\/pre>\n<p>the C# compiler can see that all of the format items are filled with strings, so it can generate a call to <code>String.Concat<\/code>:<\/p>\n<pre><code class=\"language-C#\">public static string Greet(string greeting, string name) =&gt; string.Concat(greeting, \", \", name);<\/code><\/pre>\n<p>In the general case, however, the C# compiler emits a call to <code>String.Format<\/code>. For example, if you were to write:<\/p>\n<pre><code class=\"language-C#\">public static string DescribeAsHex(int value) =&gt; $\"{value} in hex is 0x{value:X}\";<\/code><\/pre>\n<p>the C# compiler will emit code similar to the <code>string.Format<\/code> call we saw earlier:<\/p>\n<pre><code class=\"language-C#\">public static string DescribeAsHex(int value) =&gt; string.Format(\"{0} in hex is 0x{1:X}\", value, value);<\/code><\/pre>\n<p>The constant string and <code>String.Concat<\/code> examples represent about as good an output as the compiler could hope for. However, when it comes to all of the cases that end up needing <code>String.Format<\/code>, there are some limitations implied, in particular around performance but also functionality:<\/p>\n<ul>\n<li>Every time <code>String.Format<\/code> is called, it needs to parse the composite format string to find all the literal portions of the text, all of the format items, and their specifiers and alignments; somewhat ironically in the case of string interpolation, the C# compiler already had to do such parsing in order to parse the interpolated string and generate the <code>String.Format<\/code>, yet it has to be done again at run-time for each call.<\/li>\n<li>These APIs all accept arguments typed as <code>System.Object<\/code>, which means that any value types end up getting boxed in order to be passed in as an argument.<\/li>\n<li>There are <code>String.Format<\/code> overloads that accept up to three individual arguments, but for cases where more than three are needed, there&#8217;s a catch-all overload that accepts a <code>params Object[]<\/code>. That means any number of arguments more than three allocates an array.<\/li>\n<li>In order to extract the string representation to insert into a hole, the object argument&#8217;s <code>ToString<\/code> method needs to be used, which not only involves virtual (<code>Object.ToString<\/code>) or interface (<code>IFormattable.ToString<\/code>) dispatch, it also allocates a temporary string.<\/li>\n<li>These mechanisms all share a functional limitation, which is that you can only use as format items things that can be passed as <code>System.Object<\/code>. That prohibits the use of <code>ref struct<\/code>s, like <code>Span&lt;char&gt;<\/code> and <code>ReadOnlySpan&lt;char&gt;<\/code>. More and more, these types are being used as a way of improving performance by being able to represent pieces of text in a non-allocating manner, whether as a sliced span from a larger string or as text formatted into stack-allocated space or into a reusable buffer, so it&#8217;s unfortunate they can&#8217;t then be used in these larger string construction operations.<\/li>\n<li>In addition to creating <code>System.String<\/code> instances, the C# language and compiler support targeting a <code>System.FormattableString<\/code>, which is effectively a tuple of the composite format string and <code>Object[]<\/code> arguments array that would have been passed to <code>String.Format<\/code>. This enables the string interpolation syntax to be used for creating things other than <code>System.String<\/code>, as code can then take that <code>FormattableString<\/code> and its data and do something special with it; for example, the <code>FormattableString.Invariant<\/code> method accepts a <code>FormattableString<\/code> and will pass the data along with <code>CultureInfo.InvariantCulture<\/code> to <code>String.Format<\/code>, in order to perform the formatting using the invariant culture rather than the current culture. While functionally useful, this adds even more expense, as all of these objects need to be created before anything is even done with them (and beyond the allocations, <code>FormattableString<\/code> adds its own costs, such as additional virtual method calls).<\/li>\n<\/ul>\n<p>All of these issues and more are addressed by interpolated string handlers in C# 10 and .NET 6!<\/p>\n<h2>Strings, But Faster<\/h2>\n<p>&#8220;Lowering&#8221; in a compiler is the process by which the compiler effectively rewrites some higher-level or more complicated construct in terms of simpler ones or better performing ones. For example, when you <code>foreach<\/code> over an array:<\/p>\n<pre><code class=\"language-C#\">int[] array = ...;\r\nforeach (int i in array)\r\n{\r\n    Use(i);\r\n}<\/code><\/pre>\n<p>rather than emitting that as use of the array&#8217;s enumerator:<\/p>\n<pre><code class=\"language-C#\">int[] array = ...;\r\nusing (IEnumerator&lt;int&gt; e = array.GetEnumerator())\r\n{\r\n    while (e.MoveNext())\r\n    {\r\n        Use(e.Current);\r\n    }\r\n}<\/code><\/pre>\n<p>the compiler emits it as if you&#8217;d used the array&#8217;s indexer, iterating from 0 to its length:<\/p>\n<pre><code class=\"language-C#\">int[] array = ...;\r\nfor (int i = 0; i &lt; array.Length; i++)\r\n{\r\n    Use(array[i]);\r\n}<\/code><\/pre>\n<p>as this results in the smallest and fastest code.<\/p>\n<p>C# 10 addresses the afformentioned gaps in interpolated string support by allowing interpolated strings to not only be &#8220;lowered to&#8221; a constant string, a <code>String.Concat<\/code> call, or a <code>String.Format<\/code> call, but now also to a series of appends to a builder, similar in concept to how you might use a <code>StringBuilder<\/code> today to make a series of <code>Append<\/code> calls and finally extract the built string. These builders are called &#8220;interpolated string handlers&#8221;, and .NET 6 includes the following <code>System.Runtime.CompilerServices<\/code> handler type for direct use by the compiler:<\/p>\n<pre><code class=\"language-C#\">namespace System.Runtime.CompilerServices\r\n{\r\n    [InterpolatedStringHandler]\r\n    public ref struct DefaultInterpolatedStringHandler\r\n    {\r\n        public DefaultInterpolatedStringHandler(int literalLength, int formattedCount);\r\n        public DefaultInterpolatedStringHandler(int literalLength, int formattedCount, System.IFormatProvider? provider);\r\n        public DefaultInterpolatedStringHandler(int literalLength, int formattedCount, System.IFormatProvider? provider, System.Span&lt;char&gt; initialBuffer);\r\n\r\n        public void AppendLiteral(string value);\r\n\r\n        public void AppendFormatted&lt;T&gt;(T value);\r\n        public void AppendFormatted&lt;T&gt;(T value, string? format);\r\n        public void AppendFormatted&lt;T&gt;(T value, int alignment);\r\n        public void AppendFormatted&lt;T&gt;(T value, int alignment, string? format);\r\n\r\n        public void AppendFormatted(ReadOnlySpan&lt;char&gt; value);\r\n        public void AppendFormatted(ReadOnlySpan&lt;char&gt; value, int alignment = 0, string? format = null);\r\n\r\n        public void AppendFormatted(string? value);\r\n        public void AppendFormatted(string? value, int alignment = 0, string? format = null);\r\n        public void AppendFormatted(object? value, int alignment = 0, string? format = null);\r\n\r\n        public string ToStringAndClear();\r\n    }\r\n}<\/code><\/pre>\n<p>As an example of how this ends up being used, consider this method:<\/p>\n<pre><code class=\"language-C#\">public static string FormatVersion(int major, int minor, int build, int revision) =&gt;\r\n    $\"{major}.{minor}.{build}.{revision}\";<\/code><\/pre>\n<p>Prior to C# 10, this would have produced code equivalent to the following:<\/p>\n<pre><code class=\"language-C#\">public static string FormatVersion(int major, int minor, int build, int revision)\r\n{\r\n    var array = new object[4];\r\n    array[0] = major;\r\n    array[1] = minor;\r\n    array[2] = build;\r\n    array[3] = revision;\r\n    return string.Format(\"{0}.{1}.{2}.{3}\", array);\r\n}<\/code><\/pre>\n<p>We can visualize some of the aforementioned costs here by looking at this under an allocation profiler. Here I&#8217;ll use the .NET Object Allocation Tracking tool in the Performance Profiler in Visual Studio. Profiling this program:<\/p>\n<pre><code class=\"language-C#\">for (int i = 0; i &lt; 100_000; i++)\r\n{\r\n    FormatVersion(1, 2, 3, 4);\r\n}\r\n\r\npublic static string FormatVersion(int major, int minor, int build, int revision) =&gt;\r\n    $\"{major}.{minor}.{build}.{revision}\";<\/code><\/pre>\n<p>yields:<\/p>\n<p><img decoding=\"async\" class=\"alignnone\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2021\/08\/FormatVersionAllocation.jpg\" alt=\"FormatVersion allocation in profiler with C# 9, String Interpolation in C# 10 and .NET 6\" width=\"829\" height=\"255\" \/><\/p>\n<p>highlighting that we&#8217;re boxing all four integers and allocating an object[] array to store them, in addition to the resulting string we expect to see here.<\/p>\n<p>Now with C# 10 targeting .NET 6, the compiler instead produces code equivalent to this:<\/p>\n<pre><code class=\"language-C#\">public static string FormatVersion(int major, int minor, int build, int revision)\r\n{\r\n    var handler = new DefaultInterpolatedStringHandler(literalLength: 3, formattedCount: 4);\r\n    handler.AppendFormatted(major);\r\n    handler.AppendLiteral(\".\");\r\n    handler.AppendFormatted(minor);\r\n    handler.AppendLiteral(\".\");\r\n    handler.AppendFormatted(build);\r\n    handler.AppendLiteral(\".\");\r\n    handler.AppendFormatted(revision);\r\n    return handler.ToStringAndClear();\r\n}<\/code><\/pre>\n<p>Now in the profiler, we see only:<\/p>\n<p><img decoding=\"async\" class=\"alignnone\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2021\/08\/FormatVersionAllocation2.jpg\" alt=\"FormatVersion allocation in profiler with C# 9, String Interpolation in C# 10 and .NET 6\" width=\"820\" height=\"154\" \/><\/p>\n<p>with the boxing and array allocation eliminated.<\/p>\n<p>What&#8217;s going on here? The compiler:<\/p>\n<ul>\n<li>Constructs a <code>DefaultInterpolatedStringHandler<\/code>, passing in two values: the number of characters in the literal portions of the interpolated string, and the number of holes in the string. The handler can use this information to do a variety of things, such as guessing at how much space will be needed for the whole formatting operation and renting an initial buffer from <code>ArrayPool&lt;char&gt;.Shared<\/code> large enough to accomodate that.<\/li>\n<li>Issues a series of calls to append the portions of the interpolated string, calling <code>AppendLiteral<\/code> for the constant portions of the string, and calling one of the <code>AppendFormatted<\/code> overloads for the format items.<\/li>\n<li>Issues a call to the handler&#8217;s <code>ToStringAndClear<\/code> method to extract the built string (and return any <code>ArrayPool&lt;char&gt;.Shared<\/code> resources to the pool).<\/li>\n<\/ul>\n<p>If we look back at our earlier list of concerns with <code>string.Format<\/code>, we can see here how various concerns are addressed:<\/p>\n<ul>\n<li>There&#8217;s no more composite format string to be parsed at run-time: the compiler has parsed the string at compile time, and generated the appropriate sequence of calls to build up the result.<\/li>\n<li>The handler exposes a generic <code>AppendFormatted&lt;T&gt;<\/code> method, so value types will no longer be boxed in order to be appended. That has knock-on benefits as well; for example, if T is a value type, the code inside <code>AppendFormatted&lt;T&gt;<\/code> will be specialized for that particular value type, which means any interface checks or virtual\/interface dispatch performed by that method can be devirtualized and potentially even inlined. (Over the years, we&#8217;ve considered adding generic <code>String.Format<\/code> overloads, e.g. <code>Format&lt;T1, T2&gt;(string format, T1 arg, T2 arg)<\/code>, to help avoid the boxing, but such an approach can also lead to code bloat as each call site with a unique set of generic value type arguments will result in a generic specialization being created. While we may still choose to do so in the future, this approach limits such bloat by only needing one specialization of <code>AppendFormatted&lt;T&gt;<\/code> per T rather than per combination of all T1, T2, T3, etc. passed at that particular call site.)<\/li>\n<li>We&#8217;re now making one <code>AppendFormatted<\/code> call per hole, so there&#8217;s no longer an artificial cliff as to when we have to use and allocate an array to pass in more than a few arguments.<\/li>\n<li>The compiler will bind to any <code>AppendFormatted<\/code> method that accepts a compatible type as that of the data being formatted, so by exposing <code>AppendFormatted(ReadOnlySpan&lt;char&gt;)<\/code>, spans of chars can now be used in holes in interpolated strings.<\/li>\n<\/ul>\n<p>What about the intermediate string allocations that might previously have resulted from calling <code>object.ToString<\/code> or <code>IFormattable.ToString<\/code> on the format items? .NET 6 now exposes a new interface, <code>ISpanFormattable<\/code> (this interface was previously internal), which is implemented on many types in the core libraries:<\/p>\n<pre><code class=\"language-C#\">public interface ISpanFormattable : IFormattable\r\n{\r\n    bool TryFormat(Span&lt;char&gt; destination, out int charsWritten, ReadOnlySpan&lt;char&gt; format, IFormatProvider? provider);\r\n}<\/code><\/pre>\n<p>The generic <code>AppendFormatted&lt;T&gt;<\/code> overloads on <code>DefaultInterpolatedStringHandler<\/code> check to see whether the <code>T<\/code> implements this interface, and if it does, it uses it to format not into a temporary <code>System.String<\/code> but instead directly into the buffer backing the handler. On value type <code>T<\/code>s, because of generic specialization performed by the backend compiler, a check for this interface can also be performed when the assembly code is compiled, so there&#8217;s no interface dispatch for such types.<\/p>\n<p>We can see an example of the performance impact of this by running a simple benchmark:<\/p>\n<pre><code class=\"language-C#\">using BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Runtime.CompilerServices;\r\n\r\n[MemoryDiagnoser]\r\npublic class Program\r\n{\r\n    static void Main(string[] args) =&gt; BenchmarkSwitcher.FromAssemblies(new[] { typeof(Program).Assembly }).Run(args);\r\n\r\n    private int major = 6, minor = 0, build = 100, revision = 7;\r\n\r\n    [Benchmark(Baseline = true)]\r\n    public string Old()\r\n    {\r\n        var array = new object[4];\r\n        array[0] = major;\r\n        array[1] = minor;\r\n        array[2] = build;\r\n        array[3] = revision;\r\n        return string.Format(\"{0}.{1}.{2}.{3}\", array);\r\n    }\r\n\r\n    [Benchmark]\r\n    public string New()\r\n    {\r\n        var builder = new DefaultInterpolatedStringHandler(3, 4);\r\n        builder.AppendFormatted(major);\r\n        builder.AppendLiteral(\".\");\r\n        builder.AppendFormatted(minor);\r\n        builder.AppendLiteral(\".\");\r\n        builder.AppendFormatted(build);\r\n        builder.AppendLiteral(\".\");\r\n        builder.AppendFormatted(revision);\r\n        return builder.ToStringAndClear();\r\n    }\r\n}<\/code><\/pre>\n<p>On my machine, this produces:<\/p>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th style=\"text-align: right;\">Mean<\/th>\n<th style=\"text-align: right;\">Ratio<\/th>\n<th style=\"text-align: right;\">Allocated<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Old<\/td>\n<td style=\"text-align: right;\">111.70 ns<\/td>\n<td style=\"text-align: right;\">1.00<\/td>\n<td style=\"text-align: right;\">192 B<\/td>\n<\/tr>\n<tr>\n<td>New<\/td>\n<td style=\"text-align: right;\">66.75 ns<\/td>\n<td style=\"text-align: right;\">0.60<\/td>\n<td style=\"text-align: right;\">40 B<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>showing that simply recompiling yields a 40% throughput improvement and an almost 5x reduction in memory allocation. But, we can do better&#8230;<\/p>\n<p>The C# compiler doesn&#8217;t just know how to use a <code>DefaultInterpolatedStringHandler<\/code> implicitly in the lowering of an interpolated string. It also knows how to &#8220;target-type&#8221; (meaning to choose what to do based on what something is being assigned to) an interpolated string to an &#8220;interpolated string handler&#8221;, a type that implements a particular pattern the compiler knows about, and <code>DefaultInterpolatedStringHandler<\/code> implements that pattern. This means that a method can have a <code>DefaultInterpolatedStringHandler<\/code> parameter, and when an interpolated string is passed as the argument to that parameter, the compiler will generate the same construction and append calls to create and populate that handler prior to passing the handler to the method. On top of that, the method can use the <code>[InterpolatedStringHandlerArgument(...)]<\/code> attribute to get the compiler to pass other arguments into the handler&#8217;s constructor, if an appropriate constructor is provided. As shown earlier, <code>DefaultInterpolatedStringHandler<\/code> actually exposes two additional constructors beyond the ones already used in our examples, one that also accepts an <code>IFormatProvider?<\/code> used to control how formatting is accomplished, and one that further accepts a <code>Span&lt;char&gt;<\/code> that can be used as scratch space by the formatting operation (this scratch space is typically either stack-allocated or comes from some reusable array buffer easily accessed) rather than always requiring the handler to rent from the <code>ArrayPool<\/code>. That means we can write a helper method like this:<\/p>\n<pre><code class=\"language-C#\">public static string Create(\r\n    IFormatProvider? provider,\r\n    Span&lt;char&gt; initialBuffer,\r\n    [InterpolatedStringHandlerArgument(\"provider\", \"initialBuffer\")] ref DefaultInterpolatedStringHandler handler) =&gt;\r\n    handler.ToStringAndClear();<\/code><\/pre>\n<p>This method and its lack of much implementation might look a little strange&#8230; that&#8217;s because most of the work involved is actually happening at the call site. When you write:<\/p>\n<pre><code class=\"language-C#\">public static string FormatVersion(int major, int minor, int build, int revision) =&gt;\r\n    Create(null, stackalloc char[64], $\"{major}.{minor}.{build}.{revision}\");<\/code><\/pre>\n<p>the compiler lowers that to the equivalent of:<\/p>\n<pre><code class=\"language-C#\">public static string FormatVersion(int major, int minor, int build, int revision)\r\n{\r\n    Span&lt;char&gt; span = stackalloc char[64];\r\n    var handler = new DefaultInterpolatedStringHandler(3, 4, null, span);\r\n    handler.AppendFormatted(major);\r\n    handler.AppendLiteral(\".\");\r\n    handler.AppendFormatted(minor);\r\n    handler.AppendLiteral(\".\");\r\n    handler.AppendFormatted(build);\r\n    handler.AppendLiteral(\".\");\r\n    handler.AppendFormatted(revision);\r\n    return Create(null, span, ref handler);\r\n}<\/code><\/pre>\n<p>Now that we can start with stack-allocated buffer space and, in this example, won&#8217;t ever need to rent from the <code>ArrayPool<\/code>, we get numbers like this:<\/p>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th style=\"text-align: right;\">Mean<\/th>\n<th style=\"text-align: right;\">Ratio<\/th>\n<th style=\"text-align: right;\">Allocated<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Old<\/td>\n<td style=\"text-align: right;\">109.93 ns<\/td>\n<td style=\"text-align: right;\">1.00<\/td>\n<td style=\"text-align: right;\">192 B<\/td>\n<\/tr>\n<tr>\n<td>New<\/td>\n<td style=\"text-align: right;\">69.95 ns<\/td>\n<td style=\"text-align: right;\">0.64<\/td>\n<td style=\"text-align: right;\">40 B<\/td>\n<\/tr>\n<tr>\n<td>NewStack<\/td>\n<td style=\"text-align: right;\">48.57 ns<\/td>\n<td style=\"text-align: right;\">0.44<\/td>\n<td style=\"text-align: right;\">40 B<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Of course, we&#8217;re not encouraging everyone to author such a <code>Create<\/code> method on their own. That method is actually exposed on <code>System.String<\/code> in .NET 6:<\/p>\n<pre><code class=\"language-C#\">public sealed class String\r\n{\r\n    public static string Create(\r\n        IFormatProvider? provider,\r\n        [InterpolatedStringHandlerArgument(\"provider\")] ref DefaultInterpolatedStringHandler handler);\r\n\r\n    public static string Create(\r\n        IFormatProvider? provider,\r\n        Span&lt;char&gt; initialBuffer,\r\n        [InterpolatedStringHandlerArgument(\"provider\", \"initialBuffer\")] ref DefaultInterpolatedStringHandler handler);\r\n}<\/code><\/pre>\n<p>so we can instead write our example without needing any custom helper:<\/p>\n<pre><code class=\"language-C#\">public static string FormatVersion(int major, int minor, int build, int revision) =&gt;\r\n    string.Create(null, stackalloc char[64], $\"{major}.{minor}.{build}.{revision}\");<\/code><\/pre>\n<p>What about that <code>IFormatProvider?<\/code> argument? <code>DefaultInterpolatedStringHandler<\/code> is able to thread that argument through to the <code>AppendFormatted<\/code> calls, which means these <code>string.Create<\/code> overloads provide a direct (and much better performing) alternative to <code>FormattableString.Invariant<\/code>. Let&#8217;s say we wanted to use the invariant culture with our formatting example. Previously we could write:<\/p>\n<pre><code class=\"language-C#\">public static string FormatVersion(int major, int minor, int build, int revision) =&gt;\r\n    FormattableString.Invariant($\"{major}.{minor}.{build}.{revision}\");<\/code><\/pre>\n<p>and now we can write:<\/p>\n<pre><code class=\"language-C#\">public static string FormatVersion(int major, int minor, int build, int revision) =&gt;\r\n    string.Create(CultureInfo.InvariantCulture, $\"{major}.{minor}.{build}.{revision}\");<\/code><\/pre>\n<p>or if we want to use some initial buffer space as well:<\/p>\n<pre><code class=\"language-C#\">public static string FormatVersion(int major, int minor, int build, int revision) =&gt;\r\n    string.Create(CultureInfo.InvariantCulture, stackalloc char[64], $\"{major}.{minor}.{build}.{revision}\");<\/code><\/pre>\n<p>The performance difference here is even more stark:<\/p>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th style=\"text-align: right;\">Mean<\/th>\n<th style=\"text-align: right;\">Ratio<\/th>\n<th style=\"text-align: right;\">Allocated<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Old<\/td>\n<td style=\"text-align: right;\">124.94 ns<\/td>\n<td style=\"text-align: right;\">1.00<\/td>\n<td style=\"text-align: right;\">224 B<\/td>\n<\/tr>\n<tr>\n<td>New<\/td>\n<td style=\"text-align: right;\">48.19 ns<\/td>\n<td style=\"text-align: right;\">0.39<\/td>\n<td style=\"text-align: right;\">40 B<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Of course, much more than just <code>CultureInfo.InvariantCulture<\/code> can be passed in. <code>DefaultInterpolatedStringHandler<\/code> supports the same interfaces on the supplied <code>IFormatProvider<\/code> as does <code>String.Format<\/code>, so even implementations that supply an <code>ICustomFormatter<\/code> may be used. Let&#8217;s say I wanted to change my example to print all of the integer values in hex rather than in decimal. We can achieve that using format specifiers, e.g.<\/p>\n<pre><code class=\"language-C#\">public static string FormatVersion(int major, int minor, int build, int revision) =&gt;\r\n    $\"{major:X}.{minor:X}.{build:X}.{revision:X}\";<\/code><\/pre>\n<p>Now that format specifiers are provided, the compiler looks not for an <code>AppendFormatted<\/code> method that can take just the <code>Int32<\/code> value, it instead looks for one that can take both the <code>Int32<\/code> value to be formatted as well as a string format specifier. Such an overload does exist on <code>DefaultInterpolatedStringHandler<\/code>, so we end up with this code generated:<\/p>\n<pre><code class=\"language-C#\">public static string FormatVersion(int major, int minor, int build, int revision)\r\n{\r\n    var handler = new DefaultInterpolatedStringHandler(3, 4);\r\n    handler.AppendFormatted(major, \"X\");\r\n    handler.AppendLiteral(\".\");\r\n    handler.AppendFormatted(minor, \"X\");\r\n    handler.AppendLiteral(\".\");\r\n    handler.AppendFormatted(build, \"X\");\r\n    handler.AppendLiteral(\".\");\r\n    handler.AppendFormatted(revision, \"X\");\r\n    return handler.ToStringAndClear();\r\n}<\/code><\/pre>\n<p>Again, we see here that the compiler handled up front not only the parsing of the composite format string into the individual series of <code>Append<\/code> calls, but it also parsed out the format specifier to be passed as an argument to <code>AppendFormatted<\/code>. But, what if, just for fun, we wanted to output the components in, say, binary rather than in hex? There&#8217;s no format specifier that yields a binary representation of an <code>Int32<\/code>. Does that mean we can no longer use interpolated string syntax? Nope. We can write a little <code>ICustomFormatter<\/code> implementation:<\/p>\n<pre><code class=\"language-C#\">private sealed class ExampleCustomFormatter : IFormatProvider, ICustomFormatter\r\n{\r\n    public object? GetFormat(Type? formatType) =&gt; formatType == typeof(ICustomFormatter) ? this : null;\r\n\r\n    public string Format(string? format, object? arg, IFormatProvider? formatProvider) =&gt;\r\n        format == \"B\" &amp;&amp; arg is int i ? Convert.ToString(i, 2) :\r\n        arg is IFormattable formattable ? formattable.ToString(format, formatProvider) :\r\n        arg?.ToString() ??\r\n        string.Empty;\r\n}  <\/code><\/pre>\n<p>and pass that to <code>String.Create<\/code>:<\/p>\n<pre><code class=\"language-C#\">public static string FormatVersion(int major, int minor, int build, int revision) =&gt;\r\n    string.Create(new ExampleCustomFormatter(), $\"{major:B}.{minor:B}.{build:B}.{revision:B}\");<\/code><\/pre>\n<p>Neat.<\/p>\n<h3>A Note On Overloads<\/h3>\n<p>One interesting thing to note are the <code>AppendFormatted<\/code> overloads exposed on the handler. The first four are all generic and accomodate the vast majority of inputs developers can pass as format items.<\/p>\n<pre><code class=\"language-C#\">public void AppendFormatted&lt;T&gt;(T value);\r\npublic void AppendFormatted&lt;T&gt;(T value, string? format);\r\npublic void AppendFormatted&lt;T&gt;(T value, int alignment);\r\npublic void AppendFormatted&lt;T&gt;(T value, int alignment, string? format);<\/code><\/pre>\n<p>Given an <code>int value<\/code>, for example, these overloads enable format items like these:<\/p>\n<pre><code class=\"language-C#\">$\"{value}\" \/\/ formats value with its default formatting\r\n$\"{value:X2}\" \/\/ formats value as a two-digit hexademical value\r\n$\"{value,-3}\" \/\/ formats value consuming a minimum of three characters, left-aligned\r\n$\"{value,8:C}\" \/\/ formats value as currency consuming a minimum of eight characters, right-aligned<\/code><\/pre>\n<p>We could have enabled all of those just with the longest overload, if we made the alignment and format arguments optional; the compiler uses normal overload resolution to determine which <code>AppendFormatted<\/code> to bind to, and thus if we only had <code>AppendFormatted&lt;T&gt;(T value, int alignment, string? format)<\/code>, it would work fine. However, there are two reasons we didn&#8217;t do that. First, optional parameters end up baking the default values as arguments into the IL, which makes the call sites larger, and given how frequently interpolated strings are used, we wanted to minimize the call site code size as much as possible. Second, there are code quality benefits in some cases, in that when the implementation of these methods can assume the defaults for <code>format<\/code> and <code>alignment<\/code>, the resulting code can be more streamlined. So, for the generic overloads that represent the majority case for the arguments used in interpolated strings, we added all four combinations.<\/p>\n<p>There are of course some things that today can&#8217;t be represented as generics, the most prominent being <code>ref structs<\/code>. Given the importance of <code>Span&lt;char&gt;<\/code> and <code>ReadOnlySpan&lt;char&gt;<\/code> (the former of which is implicitly convertible to the latter), the handler also exposes these overloads:<\/p>\n<pre><code class=\"language-C#\">public void AppendFormatted(ReadOnlySpan&lt;char&gt; value);\r\npublic void AppendFormatted(ReadOnlySpan&lt;char&gt; value, int alignment = 0, string? format = null);<\/code><\/pre>\n<p>Given a <code>ReadOnlySpan&lt;char&gt; span = \"hi there\".Slice(0, 2);<\/code>, these overloads enable format items like these:<\/p>\n<pre><code class=\"language-C#\">$\"{span}\" \/\/ outputs the contents of the span\r\n$\"{span,4}\" \/\/ outputs the contents of the span consuming a minimum of four characters, right-aligned<\/code><\/pre>\n<p>The latter of those could have been enabled by an <code>AppendFormatted<\/code> method that only took alignment, but passing an alignment is relatively uncommon, so we decided to just have the one overload that could take both alignment and format. The <code>format<\/code> with a span is ignored, but not having this overload could result in the compiler issuing an error in some cases, and so for consistency it&#8217;s available.<\/p>\n<p>That brings us to:<\/p>\n<pre><code class=\"language-C#\">public void AppendFormatted(object? value, int alignment = 0, string? format = null);<\/code><\/pre>\n<p>Why have an <code>object<\/code>-based overload when we have a generic? It turns out there are some situations where the compiler is unable to determine a best type to use for the generic and thus would fail to bind if only the generic were offered. You can see this if you tried to write a method like this:<\/p>\n<pre><code class=\"language-C#\">public static T M&lt;T&gt;(bool b) =&gt; b ? 1 : null; \/\/ error<\/code><\/pre>\n<p>which would fail to compile because the compiler is currently unable to determine a type to use to represent the result of that ternary. However, if we instead write it as:<\/p>\n<pre><code class=\"language-C#\">public static object M(bool b) =&gt; b ? 1 : null; \/\/ ok<\/code><\/pre>\n<p>that compiles successfully, because both 1 and null can be converted to the target type of <code>object<\/code>. Thus, we expose an <code>AppendFormatted<\/code> overload for <code>object<\/code> to handle these corner cases where the type of the generic can&#8217;t be determined. But, these cases are sufficiently rare, that we only added the longest overload, with optional parameters, to use as the fallback.<\/p>\n<p>Interestingly, this then produces a problem if you try to pass a string with an alignment and a format. At that point the compiler has to choose between the <code>T<\/code>, <code>object<\/code>, and <code>ReadOnlySpan&lt;char&gt;<\/code>, and <code>string<\/code> is implicitly convertible to both <code>object<\/code> (it derives from <code>object<\/code>) and <code>ReadOnlySpan&lt;char&gt;<\/code> (there&#8217;s an implicit cast operation defined), which makes this ambiguous. To resolve that amiguity, we added the <code>string<\/code> overload that takes optional alignment and format. We also added the overload that takes just a <code>string<\/code> both because strings are incredibly common as format items and we can provide an implementation optimized specifically for string.<\/p>\n<pre><code class=\"language-C#\">public void AppendFormatted(string? value);\r\npublic void AppendFormatted(string? value, int alignment = 0, string? format = null);<\/code><\/pre>\n<h2>Interpolating into Spans<\/h2>\n<p>Thus far, we&#8217;ve seen how creating strings with string interpolation in C# gets faster and more memory efficient, and we&#8217;ve seen how we exert some control over that string interpolation via <code>String.Create<\/code>. What we haven&#8217;t yet seen is that the new C# string interpolation support goes well beyond creating new string instances. Instead, it now provides general support for using the string interpolation syntax to format into arbitrary targets.<\/p>\n<p>One of the more interesting and impactful advances in .NET in recent years has been the proliferation of spans. When it comes to text, <code>ReadOnlySpan&lt;char&gt;<\/code> and <code>Span&lt;char&gt;<\/code> have enabled significant improvements in the performance of text processing. And formatting is a key piece of that&#8230; case in point, many types in .NET now have <code>TryFormat<\/code> methods for outputting a char-based representation into a destination buffer rather than using <code>ToString<\/code> to do the equivalent into a new string instance. This will become even more prevalent now that the <code>ISpanFormattable<\/code> interface with its <code>TryFormat<\/code> method is public.<\/p>\n<p>So, let&#8217;s say I&#8217;m implementing my own type, <code>Point<\/code>, and I want to implement <code>ISpanFormattable<\/code>:<\/p>\n<pre><code class=\"language-C#\">public readonly struct Point : ISpanFormattable\r\n{\r\n    public readonly int X, Y;\r\n\r\n    public static bool TryFormat(Span&lt;char&gt; destination, out int charsWritten, ReadOnlySpan&lt;char&gt; format, IFormatProvider? provider)\r\n    {\r\n        ...\r\n    }\r\n}<\/code><\/pre>\n<p>How do I implement that <code>TryFormat<\/code> method? I could do so by formatting each component, slicing spans as I go, and in general doing everything manually, e.g.<\/p>\n<pre><code class=\"language-C#\">public bool TryFormat(Span&lt;char&gt; destination, out int charsWritten, ReadOnlySpan&lt;char&gt; format, IFormatProvider? provider)\r\n{\r\n    charsWritten = 0;\r\n    int tmpCharsWritten;\r\n\r\n    if (!X.TryFormat(destination, out tmpCharsWritten, format, provider))\r\n    {\r\n        return false;\r\n    }\r\n    destination = destination.Slice(tmpCharsWritten);\r\n\r\n    if (destination.Length &lt; 2)\r\n    {\r\n        return false;\r\n    }\r\n    \", \".AsSpan().CopyTo(destination);\r\n    tmpCharsWritten += 2;\r\n    destination = destination.Slice(2);\r\n\r\n    if (!Y.TryFormat(destination, out int tmp, format, provider))\r\n    {\r\n        return false;\r\n    }\r\n    charsWritten = tmp + tmpCharsWritten;\r\n    return true;\r\n}<\/code><\/pre>\n<p>and that&#8217;s fine, albeit a non-trivial amount of code. It&#8217;s a shame I couldn&#8217;t just use the simple string interpolation syntax to express my intent and have the compiler generate logically equivalent code for me, e.g.<\/p>\n<pre><code class=\"language-C#\">public bool TryFormat(Span&lt;char&gt; destination, out int charsWritten, ReadOnlySpan&lt;char&gt; format, IFormatProvider? provider) =&gt;\r\n    destination.TryWrite(provider, $\"{X}, {Y}\", out charsWritten);<\/code><\/pre>\n<p>In fact, you can. With C# 10 and .NET 6, the above will &#8220;just work&#8221;, thanks to the compiler&#8217;s support for custom interpolated string handlers.<\/p>\n<p>.NET 6 contains the following new extension methods on the <code>MemoryExtensions<\/code> class:<\/p>\n<pre><code class=\"language-C#\">public static bool TryWrite(\r\n    this System.Span&lt;char&gt; destination,\r\n    [InterpolatedStringHandlerArgument(\"destination\")] ref TryWriteInterpolatedStringHandler handler,\r\n    out int charsWritten);\r\n\r\npublic static bool TryWrite(\r\n    this System.Span&lt;char&gt; destination,\r\n    IFormatProvider? provider,\r\n    [InterpolatedStringHandlerArgument(\"destination\", \"provider\")] ref TryWriteInterpolatedStringHandler handler,\r\n    out int charsWritten);<\/code><\/pre>\n<p>The structure of these methods should now look familiar, taking a &#8220;handler&#8221; as a parameter that&#8217;s attributed with an <code>[InterpolatedStringHandlerArgument]<\/code> attribute referring to other parameters in the signature. This <code>TryWriteInterpolatedStringHandler<\/code> is a type designed to meet the compiler&#8217;s requirements for what an interpolated string handler looks like, in particular:<\/p>\n<ul>\n<li>It needs to be attributed with <code>[InterpolatedStringHandler]<\/code>.<\/li>\n<li>It needs to have a constructor that takes two parameters, one that&#8217;s an <code>int literalLength<\/code> and one that&#8217;s a <code>int formattedCount<\/code>. If the handler parameter has an <code>InterpolatedStringHandlerArgument<\/code> attribute, then the constructor also needs to have a parameter for each of the named arguments in that attribute, of the appropriate types and in the right order. The constructor may also optionally have an <code>out bool<\/code> as its last parameter (more on that in a moment).<\/li>\n<li>It needs to have an <code>AppendLiteral(string)<\/code> method, and it needs to have an <code>AppendFormatted<\/code> method that supports every format item type passed in the interpolated string. These methods may be void-returning or optionally bool-returning (again, more on that in a moment).<\/li>\n<\/ul>\n<p>As a result, this <code>TryWriteInterpolatedStringHandler<\/code> type ends up having a shape very similar to that of the <code>DefaultInterpolatedStringHandler<\/code>:<\/p>\n<pre><code class=\"language-C#\">[InterpolatedStringHandler]\r\npublic ref struct TryWriteInterpolatedStringHandler\r\n{\r\n    public TryWriteInterpolatedStringHandler(int literalLength, int formattedCount, Span&lt;char&gt; destination, out bool shouldAppend);\r\n    public TryWriteInterpolatedStringHandler(int literalLength, int formattedCount, Span&lt;char&gt; destination, IFormatProvider? provider, out bool shouldAppend);\r\n\r\n    public bool AppendLiteral(string value);\r\n\r\n    public bool AppendFormatted&lt;T&gt;(T value);\r\n    public bool AppendFormatted&lt;T&gt;(T value, string? format);\r\n    public bool AppendFormatted&lt;T&gt;(T value, int alignment);\r\n    public bool AppendFormatted&lt;T&gt;(T value, int alignment, string? format);\r\n\r\n    public bool AppendFormatted(ReadOnlySpan&lt;char&gt; value);\r\n    public bool AppendFormatted(ReadOnlySpan&lt;char&gt; value, int alignment = 0, string? format = null);\r\n\r\n    public bool AppendFormatted(object? value, int alignment = 0, string? format = null);\r\n\r\n    public bool AppendFormatted(string? value);\r\n    public bool AppendFormatted(string? value, int alignment = 0, string? format = null);\r\n}<\/code><\/pre>\n<p>With that type, a call like the previously shown:<\/p>\n<pre><code class=\"language-C#\">public bool TryFormat(Span&lt;char&gt; destination, out int charsWritten, ReadOnlySpan&lt;char&gt; format, IFormatProvider? provider) =&gt;\r\n    destination.TryWrite(provider, $\"{X}, {Y}\", out charsWritten);<\/code><\/pre>\n<p>will end up getting lowered to code like the following:<\/p>\n<pre><code class=\"language-C#\">public bool TryFormat(Span&lt;char&gt; destination, out int charsWritten, ReadOnlySpan&lt;char&gt; format, IFormatProvider? provider)\r\n{\r\n    var handler = new TryWriteInterpolatedStringHandler(2, 2, destination, provider, out bool shouldAppend);\r\n    _ = shouldAppend &amp;&amp;\r\n        handler.AppendFormatted(X) &amp;&amp;\r\n        handler.AppendLiteral(\", \") &amp;&amp;\r\n        handler.AppendFormatted(Y);\r\n    return destination.TryWrite(provider, ref handler, out charsWritten);\r\n}<\/code><\/pre>\n<p>There are some really interesting things happening here. First, we see the <code>out bool<\/code> from the <code>TryWriteInterpolatedStringHandler<\/code>&#8216;s constructor. The compiler is using that <code>bool<\/code> to decide whether to make any of the subsequent <code>Append<\/code> calls: if the <code>bool<\/code> is false, it short-circuits and doesn&#8217;t call any of them. That&#8217;s valuable in a situation like this because the constructor is passed both the <code>literalLength<\/code> and the <code>destination<\/code> span it&#8217;ll be writing into. If the constructor sees that the literal length is larger than the length of the destination span, it knows the interpolation can&#8217;t possibly succeed (unlike <code>DefaultInterpolatedStringHandler<\/code> which can grow to arbitrary lengths, <code>TryWriteInterpolatedStringHandler<\/code> is given the user-provided span that must contain all the data written), so why bother doing any more work? Of course, it&#8217;s possible the literals would fit but the literals plus the formatted items would not. So each <code>Append<\/code> method here also returns a <code>bool<\/code>, indicating whether the append operation succeeded, and if it didn&#8217;t (because there was no more room), the compiler again is able to short-circuit all subsequent operations. It&#8217;s also important to note that this short-circuiting doesn&#8217;t just avoid whatever work would have been done by the subsequent <code>Append<\/code> methods, it also avoids even evaluating the contents of the hole. Imagine if <code>X<\/code> and <code>Y<\/code> in these examples were expensive method invocations; this conditional evaluation means we can avoid work we know won&#8217;t be useful. Later in this post we&#8217;ll see where the benefits of that really earn their keep.<\/p>\n<p>Once all of the formatting has been done (or not done), the handler is passed to the original method the developer&#8217;s code actually called. That method&#8217;s implementation then is responsible for any final work, in this case extracting from the handler how many characters were written and whether the operation was successful, and returning that to the caller.<\/p>\n<h2>Interpolating into StringBuilders<\/h2>\n<p><code>StringBuilder<\/code> has long been one of the main ways developers create <code>String<\/code>s, with a multitude of methods exposed for mutating the instance until the data is finally copied into an immutable <code>String<\/code>. These methods include several <code>AppendFormat<\/code> overloads, e.g.<\/p>\n<pre><code class=\"language-C#\">public StringBuilder AppendFormat(string format, params object?[] args);<\/code><\/pre>\n<p>which work just like <code>string.Format<\/code>, except writing the data to the <code>StringBuilder<\/code> rather than creating a new string. Let&#8217;s consider then a variant of our <code>FormatVersion<\/code> example from earlier, this time modified to append to the builder:<\/p>\n<pre><code class=\"language-C#\">public static void AppendVersion(StringBuilder builder, int major, int minor, int build, int revision) =&gt;\r\n    builder.AppendFormat(\"{0}.{1}.{2}.{3}\", major, minor, build, revision);<\/code><\/pre>\n<p>That works of course, but it has the exact same concerns we had initially with <code>string.Format<\/code>, so someone concerned with these intermediate costs (especially if they were pooling and reusing the <code>StringBuilder<\/code> instance) might choose to write it out by hand:<\/p>\n<pre><code class=\"language-C#\">public static void AppendVersion(StringBuilder builder, int major, int minor, int build, int revision)\r\n{\r\n    builder.Append(major);\r\n    builder.Append('.');\r\n    builder.Append(minor);\r\n    builder.Append('.');\r\n    builder.Append(build);\r\n    builder.Append('.');\r\n    builder.Append(revision);\r\n}<\/code><\/pre>\n<p>You can see where this is going. .NET 6 now sports additional overloads on <code>StringBuilder<\/code>:<\/p>\n<pre><code class=\"language-C#\">public StringBuilder Append([InterpolatedStringHandlerArgument(\"\")] ref AppendInterpolatedStringHandler handler);\r\npublic StringBuilder Append(IFormatProvider? provider, [InterpolatedStringHandlerArgument(\"\", \"provider\")] ref AppendInterpolatedStringHandler handler);\r\n\r\npublic  StringBuilder AppendLine([InterpolatedStringHandlerArgument(\"\")] ref AppendInterpolatedStringHandler handler);\r\npublic  StringBuilder AppendLine(System.IFormatProvider? provider, [InterpolatedStringHandlerArgument(\"\", \"provider\")] ref AppendInterpolatedStringHandler handler)<\/code><\/pre>\n<p>With those, we can rewrite our <code>AppendVersion<\/code> example, with the simplicity of interpolated strings but the general efficiency of the individual append calls:<\/p>\n<pre><code class=\"language-C#\">public static void AppendVersion(StringBuilder builder, int major, int minor, int build, int revision) =&gt;\r\n    builder.Append($\"{major}.{minor}.{build}.{revision}\");<\/code><\/pre>\n<p>As we&#8217;ve seen, this will end up being translated by the compiler into individual append calls, each of which will append directly to the <code>StringBuilder<\/code> wrapped by the handler:<\/p>\n<pre><code class=\"language-C#\">public static void AppendVersion(StringBuilder builder, int major, int minor, int build, int revision)\r\n{\r\n    var handler = new AppendInterpolatedStringHandler(3, 4, builder);\r\n    handler.AppendFormatted(major);\r\n    handler.AppendLiteral(\".\");\r\n    handler.AppendFormatted(minor);\r\n    handler.AppendLiteral(\".\");\r\n    handler.AppendFormatted(build);\r\n    handler.AppendLiteral(\".\");\r\n    handler.AppendFormatted(revision);\r\n    builder.Append(ref handler);\r\n}<\/code><\/pre>\n<p>These new <code>StringBuilder<\/code> overloads have an additional benefit, which is that they are indeed overloads of the existing <code>Append<\/code> and <code>AppendLine<\/code> methods. When passing a non-constant interpolated string to a method with multiple overloads, one that takes a string and one that takes a valid interpolated string handler, the compiler will prefer the overload with the handler. That means, upon recompilation, any existing calls to <code>StringBuilder.Append<\/code> or <code>StringBuilder.AppendLine<\/code> that are currently being passed an interpolated string will now simply get better, appending all of the individual components directly to the builder, rather than first creating a temporary string which in turn is then appended to the builder.<\/p>\n<h2>Debug.Assert Without The Overhead<\/h2>\n<p>One of the conundrums developers sometimes face with <code>Debug.Assert<\/code> is the desire to provide lots of useful details in the assert message, while also recognizing that such details should never actually be necessary; after all, the purpose of <code>Debug.Assert<\/code> is to notify you when something that should never happen has in fact happened. String interpolation makes it easy to add lots of details to such a message:<\/p>\n<pre><code class=\"language-C#\">Debug.Assert(validCertificate, $\"Certificate: {GetCertificateDetails(cert)}\");<\/code><\/pre>\n<p>but this also means it makes it easy to pay a lot of unnecessary cost that should never be required. And while this is &#8220;only&#8221; for debug, this can have a profound impact on the performance of, for example, tests, with that overhead meaningfully detracting from a developer&#8217;s productivity, increasing how much time and resources are spent on continuous integration, and so on. Wouldn&#8217;t it be nice if we could both have this nice syntax and also avoid having to pay any of these costs in the expected 100% case where they&#8217;re not needed?<\/p>\n<p>The answer, of course, is we now can. Remember the conditionality of execution we saw earlier in the span example, where the handler was able to pass out a <code>bool<\/code> value to tell the compiler whether to short-circuit? We take advantage of that with new overloads of <code>Assert<\/code> (and <code>WriteIf<\/code> and <code>WriteLineIf<\/code>) on <code>Debug<\/code>, e.g.<\/p>\n<pre><code class=\"language-C#\">[Conditional(\"DEBUG\")]\r\npublic static void Assert(\r\n    [DoesNotReturnIf(false)] bool condition,\r\n    [InterpolatedStringHandlerArgument(\"condition\")] AssertInterpolatedStringHandler message);<\/code><\/pre>\n<p>Per my earlier comments, when <code>Debug.Assert<\/code> is called with an interpolated string argument, the compiler will now prefer this new overload over the one that takes <code>String<\/code>. For a call like the one shown (<code>Debug.Assert(validCertificate, $\"Certificate: {GetCertificateDetails(cert)}\")<\/code>), the compiler will then generate code like the following:<\/p>\n<pre><code class=\"language-C#\">var handler = new AssertInterpolatedStringHandler(13, 1, validCertificate, out bool shouldAppend);\r\nif (shouldAppend)\r\n{\r\n    handler.AppendLiteral(\"Certificate: \");\r\n    handler.AppendFormatted(GetCertificateDetails(cert));\r\n}\r\nDebug.Assert(validCertificate, handler);<\/code><\/pre>\n<p>Thus, the computation of <code>GetCertificateDetails(cert)<\/code> and the creation of the string won&#8217;t happen at all if the handler&#8217;s constructor sets <code>shouldAppend<\/code> to <code>false<\/code>, which it will do if the condition Boolean <code>validCertificate<\/code> passed in is <code>true<\/code>. In this way, we avoid doing any of the expensive work for the assert unless it&#8217;s about to fail. Pretty cool.<\/p>\n<p>This same technique is likely to be invaluable to additional APIs like those involved in logging, where for example you might only want to compute the message to be logged if the logging is currently enabled and has been set to a high enough logging level as to warrant this particular call taking effect.<\/p>\n<h3>What&#8217;s next?<\/h3>\n<p>This support is available as of .NET 6 Preview 7. We would love your feedback on it, and in particular on where else you&#8217;d like to see support for custom handlers incorporated. The most likely candidates would include places where the data is destined for something other than a string, or where the support for conditional execution would be a natural fit for the target method.<\/p>\n<p>Happy coding!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Text processing is at the heart of huge numbers of apps and services, and in .NET, that means lots and lots of System.String. String creation is so fundamental that a myriad of ways of creating them have existed since .NET Framework 1.0 was released, and more have joined the fray since. Whether via String&#8216;s constructors, [&hellip;]<\/p>\n","protected":false},"author":360,"featured_media":58792,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[685,3012,756,3009],"tags":[],"class_list":["post-33717","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dotnet","category-internals","category-csharp","category-performance"],"acf":[],"blog_post_summary":"<p>Text processing is at the heart of huge numbers of apps and services, and in .NET, that means lots and lots of System.String. String creation is so fundamental that a myriad of ways of creating them have existed since .NET Framework 1.0 was released, and more have joined the fray since. Whether via String&#8216;s constructors, [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/33717","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/users\/360"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/comments?post=33717"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/33717\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media\/58792"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media?parent=33717"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/categories?post=33717"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/tags?post=33717"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}