How to work with String.Create in C#

Take advantage of String.Create to create strings with no allocation overhead and improve the performance of your .NET 6 applications.

How to work with String.Create in C#
CalypsoArt / Getty

String handling is one of the most performance-critical areas in any application. Because strings are immutable, you can very easily accumulate many string objects very quickly, resulting in memory resource allocations that will impact application performance adversely.

When you add strings or extract substrings from a string, new string instances are created. Same goes when you perform operations such as string concatenation, which creates new string objects rather than reusing existing ones. We’ve seen how we can take advantage of the StringBuilder class when concatenating strings to reduce the number of string instances created and also reduce the allocations.

Continuing our discussion on working with strings efficiently, in this article we’ll look at how we can use the String.Create method to create strings with no resource overhead whatsoever. While string compression is a great technique for reducing resource consumption generally, String.Create is another technique you can use to handle strings efficiently—but only in certain circumstances, which we’ll discuss.

To work with the code examples provided in this article, you should have Visual Studio 2022 installed in your system. If you don’t already have a copy, you can download Visual Studio 2022 here.

Create a console application project in Visual Studio 2022

First off, let’s create a .NET Core console application project in Visual Studio. Assuming Visual Studio 2022 is installed in your system, follow the steps outlined below to create a new .NET Core console application project.

  1. Launch the Visual Studio IDE.
  2. Click on “Create a new project.”
  3. In the “Create a new project” window, select “Console App” from the list of templates displayed.
  4. Click Next.
  5. In the “Configure your new project” window shown next, specify the name and location for the new project.
  6. In the “Additional Information” window, select .NET 6.0 as the runtime and click Next
  7. Click Create.

We’ll use this .NET 6 console application project to work with strings in the sections below.

Span<T> and Memory<T>

Span<T> and Memory<T> are structs that have been added in the newer versions of .NET and that help to minimize allocations. They work as a facade over a string, array, or any contiguous block of memory. They also have read-only counterparts. The read-only counterpart of the Span<T> struct is ReadOnlySpan<T>, and the read-only counterpart of Memory<T> is ReadOnlyMemory<T>.

The String.Create method in C#

The String.Create method was added in the recent versions of C#. Here is how the Create method of the String class is declared:

public static string Create<TState> (int length, TState state, System.Buffers.SpanAction<char,TState> action);

The String.Create method requires the following:

  1. The length of the string to create
  2. The data (i.e., the state)
  3. A lambda function that can transform the state into a string

The String.Create method allocates a chunk of memory on the heap to store a sequence of characters. The first parameter of this method is the length of the final string. The second parameter is the state required to build the string object. The third and last parameter is a delegate that should work on the data in the allocated heap and generate the final string object.

When you call the String.Create method, it creates a new string that has a pre-defined size determined by the value of your length argument. Note that this is the only heap allocation that will occur when you’re using the String.Create method. Since the Create method is a member of the String class, it can access the Span<char> instance that represents the internal character data of the new string instance.

The Span<char> itself is a pointer that resides on the stack but is capable of working on the heap memory. The action lambda performs the heavy lifting of populating the string that is eventually returned to you. In other words, once the execution of the lambda function is complete, the String.Create method returns a reference to the new string instance it has created.

When to use the String.Create method

String.Create has a few specific use cases. First, you should use String.Create only in performance-critical paths. Second, you should use String.Create only when you want to build a string object when the size and format of the string are known to you. As an example, let’s say you want to log correlation Id to a log file with every method call for each and every request. You might take advantage of String.Create to build such string instances efficiently. You might also use String.Create for performance-sensitive concatenations and formatting complex strings.

Using the String.Create method

Here is a simple example of using the String.Create method:

char[] buffer = { 'a', 'e', 'i', 'o', 'u' }; 
string result = string.Create(buffer.Length, buffer, (c, b) => {
    for (int i = 0; i < c.Length; i++) c[i] = b[i];
});

Below is another example that illustrates how you can use String.Create to generate correlation Ids. Enter the following code in the Program.cs file of the console application project we created earlier.

private static readonly char[] charactersToEncode = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".ToCharArray();
private static string GetCorrelationId(long id)
    {
        return string.Create(10, id, (buffer, value) =>
        {
            char[] characters = charactersToEncode;
            buffer[9] = characters[(value >> 5) & 31];
            buffer[8] = characters[(value >> 10) & 31];
            buffer[7] = characters[(value >> 15) & 31];
            buffer[6] = characters[(value >> 20) & 31];
            buffer[5] = characters[(value >> 25) & 31];
            buffer[4] = characters[(value >> 30) & 31];
            buffer[3] = characters[(value >> 35) & 31];
            buffer[2] = characters[(value >> 40) & 31];
            buffer[1] = characters[(value >> 45) & 31];
            buffer[0] = characters[(value >> 50) & 31];
        });
    }

To get a new correlation Id, you can call the GetCorrelationId method from the Main method as shown below:

static async Task Main(string[] args)
    {
        Console.WriteLine(GetCorrelationId(DateTime.UtcNow.Ticks));
        Console.ReadKey();
    }

String.Create limitations and best practices

When using String.Create, you should first of all keep its limitations in mind. You should know the size of the string you want to create in advance, which will require knowing the length of the state objects that the final string will be composed of.

There are also two best practices you should adhere to when working with the String.Create method. First, it’s wise to benchmark the performance of your application to ensure that using String.Create actually yields better results. Second, if you're using multiple objects for the state, be sure to take advantage of ValueTuples.

Finally, note that String.Create might not be a good choice in certain scenarios. You should not use String.Create when readability or culture is important for your application or your development team.

So, whether you should use String.Create or not depends on the trade-offs between its downsides and performance benefits. My advice is, benchmark your code, see the results, and then decide. I’ll write more on writing high performance code in future posts here.

Copyright © 2022 IDG Communications, Inc.

How to choose a low-code development platform