如何在C#中使用String.Create(附实例)

362 阅读5分钟

利用String.Create的优势,创建没有分配开销的字符串,提高你的.NET 6应用程序的性能。

字符串处理是任何应用程序中性能最关键的领域之一。因为字符串是不可变的,你可以非常容易地迅速积累许多字符串对象,从而导致内存资源分配,对应用程序的性能产生不利影响。

当你添加字符串或从一个字符串中提取子串时,会创建新的字符串实例。当你执行诸如字符串连接的操作时也是如此,它创建了新的字符串对象,而不是重复使用现有的对象。我们已经看到,在连接字符串时,我们可以利用StringBuilder类来减少创建的字符串实例的数量,也可以减少分配。

继续我们关于高效处理字符串的讨论,在这篇文章中,我们将看看我们如何利用String.Create方法来创建字符串,而没有任何资源开销。虽然字符串压缩是一个很好的减少资源消耗的技术,但String.Create是另一个你可以用来有效处理字符串的技术--但只是在某些情况下,我们将讨论这个问题。

要使用本文提供的代码示例,你的系统中应该安装有Visual Studio 2022。如果你还没有副本,你可以在这里下载Visual Studio 2022

在Visual Studio 2022中创建一个控制台应用程序项目

首先,让我们在Visual Studio中创建一个.NET核心控制台应用程序项目。假设Visual Studio 2022已经安装在你的系统中,按照下面的步骤创建一个新的.NET Core控制台应用程序项目:

  1. 启动Visual Studio IDE。
  2. 点击 "创建一个新项目"。
  3. 在 "创建一个新项目 "窗口中,从显示的模板列表中选择 "控制台应用程序"。
  4. 点击 "下一步"。
  5. 在接下来显示的 "配置你的新项目 "窗口中,指定新项目的名称和位置。
  6. 在 "其他信息 "窗口中,选择.NET 6.0作为运行时间,然后点击下一步。
  7. 点击 "创建"。

我们将在下面的章节中使用这个.NET 6控制台应用程序项目来处理字符串。

Span和Memory

Span和Memory是在较新版本的.NET中添加的结构,它们有助于最大限度地减少分配。它们在字符串、数组或任何连续的内存块上作为一个门面工作。它们也有只读的对应物。Span结构的只读对应物是ReadOnlySpan,而Memory的只读对应物是ReadOnlyMemory。

C#中的String.Create方法

String.Create方法是在C#的最新版本中添加的。下面是String类的Create方法的声明方式:

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

String.Create方法需要以下条件:

  1. 要创建的字符串的长度
  2. 数据(即,状态)
  3. 一个可以将状态转化为字符串的lambda函数

String.Create方法在堆上分配了一大块内存来存储一串字符。这个方法的第一个参数是最终字符串的长度。第二个参数是构建字符串对象所需的状态。第三个也是最后一个参数是一个委托,它应该在分配的堆中的数据上工作并生成最终的字符串对象。

当你调用String.Create方法时,它会创建一个新的字符串,它有一个预先定义的大小,由你的length参数的值决定。注意,这是你在使用String.Create方法时唯一会发生的堆分配。由于Create方法是String类的成员,它可以访问Span实例,该实例代表新字符串实例的内部字符数据。

Span本身是一个驻留在堆栈中的指针,但能够在堆内存中工作。动作lambda执行了填充字符串的重任,最终返回给你。换句话说,一旦lambda函数执行完毕,String.Create方法就会返回一个它所创建的新字符串实例的引用。

何时使用String.Create方法

String.Create有几个特定的使用情况。首先,你应该只在性能关键的路径中使用String.Create。第二,只有当你想建立一个字符串对象时,当字符串的大小和格式为你所知时,你才应该使用String.Create。举个例子,假设你想把每个请求的每个方法调用的相关Id记录到一个日志文件中。你可以利用String.Create来有效地建立这样的字符串实例。你也可以使用String.Create来进行性能敏感的连接和格式化复杂的字符串。

使用String.Create方法

下面是一个使用String.Create方法的简单例子:

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]; 
});

下面是另一个例子,说明了如何使用String.Create来生成相关的Ids。在我们先前创建的控制台应用程序项目的Program.cs文件中输入以下代码。

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];
        });
    }

为了得到一个新的相关ID,你可以从Main方法中调用GetCorrelationId方法,如下所示。

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

String.Create的限制和最佳实践

当使用String.Create时,你首先应该牢记它的限制。你应该事先知道你要创建的字符串的大小,这就需要知道最终字符串将由的状态对象的长度。

在使用String.Create方法时,还有两个最佳实践你应该遵守。首先,对你的应用程序的性能进行基准测试是明智的,以确保使用String.Create实际产生更好的结果。其次,如果你要使用多个对象的状态,一定要利用ValueTuples的优势。

最后,请注意,在某些情况下,String.Create可能不是一个好的选择。当可读性或文化对你的应用程序或你的开发团队很重要时,你不应该使用String.Create。

所以,你是否应该使用String.Create取决于其缺点和性能优势之间的权衡。我的建议是,对你的代码进行基准测试,看看结果,然后再决定。我将在以后的文章中写更多关于编写高性能代码的内容。