字符串插值与 String.Format

String Interpolation vs String.Format

使用字符串插值之间是否存在明显的性能差异:

myString += $"{x:x2}";

vs String.Format()?

myString += String.Format("{0:x2}", x);

我问是因为 Resharper 正在提示修复,我以前被骗过。

显着是相对的。但是:字符串插值在编译时变成 string.Format(),因此它们最终应该得到相同的结果。

虽然存在细微差别:正如我们从 this 问题中可以看出的那样,格式说明符中的字符串连接会导致额外的 string.Concat() 调用。

字符串插值在 compile-time.

处变为 string.Format()

同样在 string.Format 中,您可以为单个参数指定多个输出,并为单个参数指定不同的输出格式。但我猜字符串插值更具可读性。所以,这取决于你。

a = string.Format("Due date is {0:M/d/yy} at {0:h:mm}", someComplexObject.someObject.someProperty);

b = $"Due date is {someComplexObject.someObject.someProperty:M/d/yy} at {someComplexObject.someObject.someProperty:h:mm}";

有一些性能测试结果https://koukia.ca/string-interpolation-vs-string-format-string-concat-and-string-builder-performance-benchmarks-c1dad38032a

问题是关于性能的问题,但是标题只是说"vs",所以我觉得有必要再补充几点,虽然有些观点很自以为是。

  • 本地化

    • 由于内联代码的性质,字符串插值无法本地化。汉化前已变成string.Format。但是,有相应的工具(例如 ReSharper)。
  • 可维护性(我的意见)

    • string.Format 更具可读性,因为它侧重于 句子 我想表达的内容,例如在构建漂亮且有意义的错误消息时。使用 {N} 占位符给了我更大的灵活性,以后修改它也更容易。
    • 另外,插值中的内联格式说明符容易被误读,并且在更改时容易与表达式一起删除。
    • 当使用复杂而长的表达式时,插值很快变得更加难以阅读和维护,因此从这个意义上讲,当代码不断发展并变得更加复杂时,它无法很好地扩展。 string.Format 不太容易出现这种情况。
    • 归根结底,这一切都是关于关注点的分离:我不喜欢将它应该如何呈现应该是什么混为一谈呈现.

因此,基于这些,我决定在我的大部分代码中坚持使用 string.Format。但是,我已经准备了一种扩展方法,以获得更 流畅 的编码方式,我更喜欢这种方式。该扩展的实现是一个one-liner,它在使用中看起来很简单。

var myErrorMessage = "Value must be less than {0:0.00} for field {1}".FormatWith(maximum, fieldName);

插值是一项很棒的功能,请不要误会我的意思。但是 IMO 它在那些缺少 string.Format-like 特性的语言中表现最好,例如 JavaScript.

答案是肯定的,也不是。 ReSharper 通过不显示 third 变体来愚弄你,这也是性能最高的。列出的两个变体生成相同的 IL 代码,但以下确实会有所提升:

myString += $"{x.ToString("x2")}";

完整测试代码

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Diagnostics.Windows;
using BenchmarkDotNet.Running;

namespace StringFormatPerformanceTest
{
    [Config(typeof(Config))]
    public class StringTests
    {
        private class Config : ManualConfig
        {
            public Config() => AddDiagnoser(MemoryDiagnoser.Default, new EtwProfiler());
        }

        [Params(42, 1337)]
        public int Data;

        [Benchmark] public string Format() => string.Format("{0:x2}", Data);
        [Benchmark] public string Interpolate() => $"{Data:x2}";
        [Benchmark] public string InterpolateExplicit() => $"{Data.ToString("x2")}";
    }

    class Program
    {
        static void Main(string[] args)
        {
            var summary = BenchmarkRunner.Run<StringTests>();
        }
    }
}

测试结果

|              Method | Data |      Mean |  Gen 0 | Allocated |
|-------------------- |----- |----------:|-------:|----------:|
|              Format |   42 | 118.03 ns | 0.0178 |      56 B |
|         Interpolate |   42 | 118.36 ns | 0.0178 |      56 B |
| InterpolateExplicit |   42 |  37.01 ns | 0.0102 |      32 B |
|              Format | 1337 | 117.46 ns | 0.0176 |      56 B |
|         Interpolate | 1337 | 113.86 ns | 0.0178 |      56 B |
| InterpolateExplicit | 1337 |  38.73 ns | 0.0102 |      32 B |

InterpolateExplicit() 方法更快,因为我们现在明确告诉编译器使用 string。无需 box 对象 进行格式化。拳击确实很费钱。另外,请注意我们稍微减少了分配。

也许提晚了,但没有发现其他人提到它:我注意到你问题中的 += 运算符。看起来你正在创建一些循环执行此操作的东西的十六进制输出。

在字符串 (+=) 上使用 concat,尤其是在循环中可能会导致难以发现的问题:OutOfMemoryException 在分析转储时会显示内部有大量可用内存!

会发生什么?

  1. 内存管理将寻找一个连续的 space 足以作为结果字符串。
  2. 那里写的连接字符串。
  3. 用于存储原始左侧变量值的space已释放。

请注意,在步骤 #1 中分配的 space 肯定大于在步骤 #3 中释放的 space。

在下一个循环中,同样的事情发生,依此类推。 假设在每个循环中将 10 字节长的字符串添加到最初 20 字节长的字符串 3 次,我们的内存会是什么样子?

[20 字节空闲]X1[30 字节空闲]X2[40 字节空闲]X2[分配 50 字节]

(因为几乎可以肯定在我放置的循环中还有其他命令使用内存 Xn-s 来演示它们的内存分配。这些可能被释放或仍然被分配,跟我来。)

如果在下一次分配时MM发现没有足够大的连续内存(60字节) 然后它会尝试从 OS 或通过在其出口中重组免费 space 来获取它。 X1 和 X2 将被移动到某处(如果可能的话)并且 20+30+40 连续块变得可用。需要时间但有空。

但是 如果块大小达到 88kb(google 为什么是 88kb)它们将被分配到 大对象堆。这里的空闲块将不再被压缩。

因此,如果您的字符串 += 操作结果超过此大小(例如,您正在构建 CSV 文件或以这种方式在内存中渲染某些内容),上述循环将导致空闲内存块的大小不断增加,它们的总和可以达到千兆字节,但是您的应用程序将因 OOM 而终止,因为它无法分配一个可能小至 1Mb 的块,因为 none 个块足够大了:)

抱歉,解释得太长了,但它发生在几年前,是一个艰难的教训。从那时起,我就反对不恰当地使用字符串连接。

您应该注意到,在 C#10 和 .NET 6 中对字符串插值进行了重大优化 - String Interpolation in C# 10 and .NET 6

我一直在将我对字符串格式以及字符串连接的所有用法迁移到使用字符串插值。

我同样关心不同方法之间的内存分配差异,如果不是更多的话。我发现,在处理少量字符串时,字符串插值几乎总是在速度和内存分配方面获胜。如果您有未确定(在设计时不知道)数量的字符串,您应该始终使用 System.Text.StringBuilder.Append(xxx)System.Text.StringBuilder.AppendFormat(xxx)

此外,我会标注您使用 += 进行字符串连接。非常小心,只对 小数量 小字符串 .

这样做

在微软的网站上有一条关于 String.Format 的重要说明: https://docs.microsoft.com/en-us/dotnet/api/system.string.format?view=net-6.0

" 如果您的语言支持,您可以使用内插字符串,而不是调用 String.Format 方法或使用复合格式字符串。内插字符串是包含内插表达式的字符串。每个内插表达式解析为表达式的值,并在分配字符串时包含在结果字符串中。"