C#String·

[C#] C# String.Format() 和 StringBuilder

使用 Unity 5.6.0f3 使用相同脚本测试各种集合类型如列表、字典、数组等在 Foreach 循环中的垃圾生成情况。

英文原文:https://codewithshadman.com/c-stringformat-and-stringbuilder/

最近,我看到一些代码,看起来像这样:

StringBuilder builder = new StringBuilder();
builder.Append(String.Format("{0} {1}", firstName, lastName));
// Do more with builder...

现在,我不想争论 String.Concat() 在这里如何提高性能。 String.Format() 允许更轻松地本地化代码,这里就使用它来实现此目的。真正的问题是应该使用 StringBuilder.AppendFormat() 来代替:

StringBuilder builder = new StringBuilder();
builder.AppendFormat("{0} {1}", firstName, lastName);
// Do more with builder...

这很重要的原因是,在内部,String.Format() 实际上创建了一个 StringBuilder 并调用 StringBuilder.AppendFormat()! String.Format() 的实现如下:

public static string Format(IFormatProvider provider, string format, params object[] args)
{
  if (format == null || args == null)
   throw new ArgumentNullException((format == null ? "format" : "args"));

  StringBuilder builder = new StringBuilder(format.Length + (args.Length * 8));
  builder.AppendFormat(provider, format, args);
  return builder.ToString();
}

您可以在.NET Core运行时的源代码中看到实际的实现。这是相同的链接:String.Manipulation.cs

在 String.Manipulation.cs 文件中,您将找到 Format 方法:

从上面的代码中,可以看到Format方法调用了一个内部方法FormatHelper。 现在,FormatHelper 方法使用 StringBuilder 通过 StringBuilderCache 格式化文本。

事实证明,格式化逻辑实际上是在StringBuilder.AppendFormat()中实现的。因此,原始代码实际上导致在不需要时使用第二个 StringBuilder。

如果您试图避免通过使用 String.Format() 连接字符串来创建 StringBuilder,了解这一点也很重要。例如:

string nameString = "<td>" + String.Format("{0} {1}", firstName, lastName) + "</td>"
  + "<td>" + String.Format("{0}, {1}", id, department) + "</td>";

如果格式化字符串的大小大于 StringBuilderCache 中使用的 MaxBuilderSize(设置为 360),该代码实际上会创建两个 StringBuilder。因此,创建一个 StringBuilder 并使用 AppendFormat() 会更高效:

StringBuilder nameBuilder = new StringBuilder();
nameBuilder.Append("<td>");
nameBuilder.AppendFormat("{0} {1}", firstName, lastName);
nameBuilder.Append("</td>");
nameBuilder.Append("<td>");
nameBuilder.AppendFormat("{0}, {1}", id, department);
nameBuilder.Append("</td>");
string nameString = nameBuilder.ToString();

我决定进行一些性能测试来验证我的说法。首先,我对代码进行了计时,演示了 StringBuilder 存在的原因:

const int LOOP_SIZE = 10000;
const string firstName = "Shadman";
const string lastName = "Kudchikar";
const int id = 1;
const string department = ".NET Team";

static void PerformanceTest1()
{
  string nameString = String.Empty;

  for (int i = 0; i < LOOP_SIZE; i++)
    nameString += String.Format("{0} {1}", firstName, lastName);
}

上面的代码创建一个新字符串,然后在 for 循环内将其连接起来。这会导致每次传递时创建两个新字符串——一个来自 String.Format(),另一个来自串联。这是非常低效的。

接下来,我测试了修改为使用带有 String.Format() 的 StringBuilder 的相同代码:

static void PerformanceTest2()
{
  StringBuilder builder = new StringBuilder((firstName.Length + lastName.Length + 1) * LOOP_SIZE);

  for (int i = 0; i < LOOP_SIZE; i++)
    builder.Append(String.Format("{0} {1}", firstName, lastName));

  string nameString = builder.ToString();
}

最后,我测试了使用 StringBuilder.AppendFormat() 而不是 String.Format() 的代码:

static void PerformanceTest3()
{
	 StringBuilder builder = new StringBuilder((firstName.Length + lastName.Length + 1) * LOOP_SIZE);
	
	 for (int i = 0; i < LOOP_SIZE; i++)
	   builder.AppendFormat("{0} {1}", firstName, lastName);
	
	 string nameString = builder.ToString();
}

这三种方法的运行时间如下:

对于 .NET Framework:

PerformanceTest1: 0.21045 seconds PerformanceTest2: 0.001585 seconds PerformanceTest3: 0.0010846 seconds

对于 .NET Core:

PerformanceTest1: 0.173821 seconds PerformanceTest2: 0.0012753 seconds PerformanceTest3: 0.0007812 seconds

显然,在不使用 StringBuilder 的情况下在循环中连接字符串的效率低得惊人。而且,删除对 String.Format 的调用还可以提高性能。

接下来我测试了以下两种方法:

static void PerformanceTest4()
{
  string htmlString;
  for (int i = 0; i < LOOP_SIZE; i++)
    htmlString = "<td>" + String.Format("{0} {1}", firstName, lastName) + "</td><td>"
      + String.Format("{0}, {1}", id, department) + "</td>";
}
static void PerformanceTest5()
{
  StringBuilder builder = new StringBuilder(256);

  string htmlString;
  for (int i = 0; i < LOOP_SIZE; i++)
  {
    builder.Append("<td>");
    builder.AppendFormat("{0} {1}", firstName, lastName);
    builder.Append("</td><td>");
    builder.AppendFormat("{0}, {1}", id, department);
    builder.Append("</td>");
    htmlString = builder.ToString();
    builder.Length = 0;
  }
}

这两种方法的运行时间如下:

对于 .NET Framework:

PerformanceTest4: 0.0050095 seconds PerformanceTest5: 0.0044467 seconds

对于 .NET Core:

PerformanceTest4: 0.0036363999999999997 seconds PerformanceTest5: 0.0019971 seconds

正如您所看到的,了解何时使用 String.Format 以及何时使用 StringBuilder.AppendFormat() 非常重要。虽然可以实现的性能提升相当小,但它们太容易编码了。

您可以在此处下载性能测试:https://github.com/kudchikarsk/string-format-performance-test。

关于

Copyright © 2024. All rights reserved.