字符串插值 vs 字符串格式化

178

使用字符串插值和使用字符串连接符之间有明显的性能差异吗?

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

vs String.Format()?

与String.Format()相比呢?

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

我只是问一下,因为ReSharper正在提示修复,而我以前被愚弄过。


69
@Blorgbeard 老实说,我很懒。而且我觉得如果你们中的一位知道答案的话,这会花费更少的时间。 - Krythic
46
我喜欢的是,当我最初问这个问题时,它被投票反对到了谷底,但现在两年过去了,得票数已经达到了+21。 - Krythic
80
说真的,有人怎么会怀疑这个问题的实用性呢?如果每个人都必须“自己尝试并查看”,那要浪费多少人力资源啊!即使只需要 5 分钟,也要把这个时间乘以迄今已经查看了此问题的 10,000 多名开发人员。而且当一个同事怀疑你的结果时,你该怎么办呢?再做一遍吗?还是直接引用这篇 Stack Overflow 帖子呢?这正是它的作用所在。 - BTownTKD
11
@BTownTKD 这是 Stackoverflow 的典型行为。如果有人使用该网站的预期目的,他们会立即被排斥。这也是我认为我们应该被允许集体禁止账户的原因之一。许多人根本不配在这个网站上。 - Krythic
9
我认为"frivolous"指的是对意义或上下文没有影响的事情(例如,一个打字错误)。但是,在标题中添加关键词会使问题更易被找到。这对于未来的读者来说是有帮助的,这对我来说并不是无关紧要的。正如我之前所提到的,目前这个标题非常广泛和模糊。 - StayOnTarget
显示剩余5条评论
7个回答

78

可感知性是相对的。然而: 字符串插值在编译时被转换为string.Format(),因此它们应该得到相同的结果。

不过,还有一些微妙的差别:我们可以从这个问题中看出,在格式说明符中的字符串连接会导致额外的string.Concat()调用。


6
实际上,在某些情况下(例如使用int时),字符串插值可能会编译成字符串连接。var a = "hello"; var b = $"{a} world"; 编译为字符串连接。 var a = "hello"; var b = $"{a} world {1}"; 编译为字符串格式化。 - Omar Muscatello

59
答案既是肯定的也是否定的。 ReSharper 没有显示第三种变体,这也是最高效的一种。列出的两种变体会产生相同的 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 |

新的测试结果 (.NET 6)

.NET 6.0.9.41905, X64 RyuJIT AVX2 上重新运行了测试。

|              Method | Data |      Mean |   Gen0 | Allocated |
|-------------------- |----- |----------:|-------:|----------:|
|              Format |   42 |  37.47 ns | 0.0089 |      56 B |
|         Interpolate |   42 |  57.61 ns | 0.0050 |      32 B |
| InterpolateExplicit |   42 |  11.46 ns | 0.0051 |      32 B |
|              Format | 1337 |  39.49 ns | 0.0089 |      56 B |
|         Interpolate | 1337 |  59.98 ns | 0.0050 |      32 B |
| InterpolateExplicit | 1337 |  12.85 ns | 0.0051 |      32 B |

InterpolateExplicit() 方法更快,因为我们现在明确告诉编译器使用 string。不需要对要格式化的 object 进行 boxing 。装箱的成本确实非常高昂。还请注意,NET 6 对所有方法都减少了 CPU 和内存分配。

新的测试结果(.NET 7)

.NET 7.0.122.56804, X64 RyuJIT AVX2 上重新运行了测试。

|              Method | Data |      Mean |   Gen0 | Allocated |
|-------------------- |----- |----------:|-------:|----------:|
|              Format |   42 |  41.04 ns | 0.0089 |      56 B |
|         Interpolate |   42 |  65.82 ns | 0.0050 |      32 B |
| InterpolateExplicit |   42 |  12.19 ns | 0.0051 |      32 B |
|              Format | 1337 |  41.02 ns | 0.0089 |      56 B |
|         Interpolate | 1337 |  59.61 ns | 0.0050 |      32 B |
| InterpolateExplicit | 1337 |  13.28 ns | 0.0051 |      32 B |

.NET 6以来没有重大变化。


2
第三个变量如果x为空将会崩溃。 - Pang
1
为什么在这种情况下要使用插值,而不是只使用 myString += x.ToString("x2"); - Alexandre
显然,你不会这样做。为了举例说明,我选择了非常简单的字符串格式化。 - l33t
2
查看反编译的IL代码 sharplab.io/...。Format和Interpolate具有完全相同的内容。它使用了string.Format函数。InterpolateExplicit中的字符串插值是多余的,它只是调用Data.ToString("x2")...并检查是否为空。奇怪... Data.ToString("x2")会返回null吗? - marbel82
2
我在@meziantou发现了一篇有趣的博客文章[插入字符串:高级用法](https://www.meziantou.net/interpolated-strings-advanced-usages.htm) - marbel82
显示剩余7条评论

15
请注意,在C# 10和.NET 6中,字符串插值进行了重大优化- C# 10和.NET 6中的字符串插值
我已将所有使用的字符串格式化和字符串连接的方式迁移到使用字符串插值。
如果您在不同方法之间关注内存分配差异,我会更加关注,甚至更加担心。我发现,当处理较少数量的字符串时,字符串插值在速度和内存分配方面几乎总是胜出的。 如果您有一个未确定的字符串数量(设计时未知),则应始终使用System.Text.StringBuilder.Append(xxx)System.Text.StringBuilder.AppendFormat(xxx)
此外,我要提醒您对于字符串连接的使用+=。请非常小心,仅针对小型字符串小数量这样使用。

5
字符串插值在编译时转换为`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


3
字符串内插有时会转换为String::Format,有时则转换为String::Concat。对于该页面上的性能测试,我认为其意义并不是特别明显:你传递给这些方法的参数数量是不确定的。Concat并不总是最快的,而StringBuilder也并不总是最慢的。 - Matthias Burger

5
问题是关于性能的,然而标题只是说“vs”,因此我觉得需要补充一些细节,其中一些有些主观。
- 本地化
- 由于其内联代码的特性,字符串插值无法本地化。在本地化之前,必须将其转换为 `string.Format`。不过,有相应的工具(例如 `ReSharper`)可以完成这项任务。
- 可维护性(我的观点)
- `string.Format` 更易读,因为它专注于我想表达的句子,例如构建一个好看且有意义的错误消息。使用 `{N}` 占位符可以给我更多的灵活性,并且以后修改起来也更容易。 - 另外,插值中内嵌的格式说明符易于误读,在进行更改时很容易与表达式一起删除。 - 当使用复杂且长的表达式时,插值快速变得更难阅读和维护,因此在这个意义上,它在代码演进和变得更加复杂时不易扩展。相比之下,`string.Format` 更不容易出现这种情况。 - 最终,这一切都与责任分离有关:我不喜欢混合“如何呈现”与“应该呈现什么”。
因此,基于这些原因,我决定在大多数情况下坚持使用 `string.Format`。不过,我准备了一个扩展方法,以更加“流畅”的方式编写代码。这个扩展的实现只有一行代码,并且使用起来非常简单。
var myErrorMessage = "Value must be less than {0:0.00} for field {1}".FormatWith(maximum, fieldName);

插值是一个很棒的功能,不要误解我的意思。但我认为,它在那些缺少类似于 string.Format 功能的语言中表现最好,例如 JavaScript。


6
对于可维护性,我不太同意;虽然 ReSharper 使得将插入的值与其对应的索引(反之亦然)相匹配变得更加容易,但我认为如果你开始重新排列格式,弄清楚“{3}”是 X 还是 Y,仍然需要更多的认知负荷。Madlibs 示例:$"It was a {adjective} day in {month} when I {didSomething}"string.Format("It was a {0} day in {1} when I {2}", adjective, month, didSomething) --> $"I {didSomething} on a {adjective} {month} day"string.Format("I {2} on a {0} {1} day", adjective, month, didSomething) - drzaus
1
@drzaus 感谢分享您的想法。您有很好的观点,但仅当我们仅使用简单且命名良好的本地变量时才是正确的。我看到的很多次是将复杂表达式、函数调用等放入插值字符串中。使用 string.Format,我认为您更不容易遇到此问题。但无论如何,这就是为什么我强调这是我的观点 :) - Zoltán Tamási

3
也许已经晚了,但我没有发现其他人提到:我注意到你的问题中使用了+=运算符。看起来你正在通过在循环中执行此操作来创建某个东西的十六进制输出。
在字符串上使用concat(+=),特别是在循环中,可能会导致难以发现的问题:分析转储时的OutOfMemoryException将显示大量可用内存!
发生了什么?
1. 内存管理将寻找足够结果字符串的连续空间。 2. 连接的字符串被写入其中。 3. 用于存储原始左手边变量的值的空间被释放。
请注意,第1步分配的空间肯定比第3步释放的空间要大。
在下一个循环中,相同的情况发生,以此类推。如果在每个循环中添加长度为10个字节的字符串到原始长度为20个字节的字符串3次,那么我们的内存将如何呈现?
[20个字节的空闲] X1 [30个字节的空闲] X2 [40个字节的空闲] X2 [分配了50个字节]
(因为几乎可以确定,在循环期间有其他命令使用内存,所以我放置了Xn-s来演示它们的内存分配。这些可能被释放或仍然分配,跟着我。)
如果在下一次分配中,内存管理找不到足够的大连续内存(60个字节),那么它将尝试从操作系统获取它,或通过重构其出口中的空闲空间来获取它。如果可能的话,X1和X2将被移动到其他地方,一个20+30+40的连续块变为可用状态。这需要时间,但是可用。
但是,如果块大小达到88kb(为什么是88kb请谷歌了解),它们将在Large Object Heap上分配。这里的空闲块不会再被压缩。
因此,如果你的字符串 += 操作结果超过了这个大小(例如你正在以这种方式构建CSV文件或者渲染内存中的内容),那么上述循环将导致不断增长的自由内存块,它们的总和可能达到几十亿字节,但是你的应用程序将因无法分配一个可能只有1Mb的块而终止,因为这些块中没有一个足够大:)
抱歉解释得有点长,但几年前发生过这样的事情,教训很深刻。我从那时起就一直在与不适当使用字符串连接进行斗争。

1

在 Microsoft 网站上有关于 String.Format() 的重要说明:

https://learn.microsoft.com/en-us/dotnet/api/system.string.format?view=net-6.0#remarks

不必调用String.Format方法或使用复合格式字符串,如果您的语言支持,可以使用插值字符串。插值字符串是包含插值表达式的字符串。每个插值表达式都会根据表达式的值解析,并在字符串被赋值时包含在结果字符串中。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接