什么时候使用String.Format比字符串串联更好?


Answers:


115

在C#6之前

老实说,我认为第一个版本更简单-尽管我将其简化为:

xlsSheet.Write("C" + rowIndex, null, title);

我怀疑其他答案可能涉及性能下降,但是老实说,如果存在的话,它将是最小的-而且此串联版本不需要解析格式字符串。

格式字符串对于本地化等而言非常有用,但是在这种情况下,这种连接更为简单,并且效果也很好。

使用C#6

字符串插值使许多事情在C#6中更易于阅读。在这种情况下,第二个代码变为:

xlsSheet.Write($"C{rowIndex}", null, title);

这可能是最好的选择,IMO。



我知道我知道。它是开玩笑制作的(之前已经读过btw的链接,这是一个很好的阅读)
2013年

乔恩 我一直是里希特先生的粉丝,并且虔诚地遵循了拳击等方面的指导。但是,在阅读了您的(旧)文章之后,我现在是一位convert依者。谢谢
stevethethread 2014年


4
现在C#6可用了,您可以使用新的字符串插值语法来实现更容易阅读的内容:xlsSheet.Write($"C{rowIndex}", null, title);
HotN

158

我最初的偏好(来自C ++背景)是String.Format。由于以下原因,我稍后将其删除:

  • 字符串连接可以说是“更安全”的。在我身上(我已经看到过在其他几个开发人员身上发生过),我删除了一个参数,或者错误地弄乱了参数顺序。编译器将不会根据格式字符串检查参数,并且您最终会遇到运行时错误(即,如果您有幸没有将它用晦涩的方法(例如记录错误),则会出现错误。使用串联时,删除参数不太容易出错。您可能会说出错的机会很小,但是可能会发生。

-字符串串联允许空值,String.Format不允许。编写“ s1 + null + s2”不会中断,只是将null值视为String.Empty。好吧,这可能取决于您的特定情况-在某些情况下,您想要一个错误,而不是静默忽略空的FirstName。但是,即使在这种情况下,我个人更喜欢自己检查null并抛出特定错误,而不是从String.Format获得的标准ArgumentNullException。

  • 字符串连接的性能更好。上面的一些帖子已经提到了这一点(实际上并没有解释为什么,这决定了我写这篇文章:)。

想法是.NET编译器足够聪明,可以转换这段代码:

public static string Test(string s1, int i2, int i3, int i4, 
        string s5, string s6, float f7, float f8)
{
    return s1 + " " + i2 + i3 + i4 + " ddd " + s5 + s6 + f7 + f8;
}

对此:

public static string Test(string s1, int i2, int i3, int i4,
            string s5, string s6, float f7, float f8)
{
    return string.Concat(new object[] { s1, " ", i2, i3, i4, 
                    " ddd ", s5, s6, f7, f8 });
}

在String.Concat的幕后发生的事情很容易猜测(使用Reflector)。数组中的对象通过ToString()转换为它们的字符串。然后计算总长度,仅分配一个字符串(具有总长度)。最后,每个字符串都通过wstrcpy以一些不安全的代码片段复制到结果字符串中。

原因String.Concat更快吗?好吧,我们都可以看看String.Format正在做什么-对于处理格式字符串所需的代码量,您会感到惊讶。最重要的是(我已经看到了有关内存消耗的注释),String.Format在内部使用StringBuilder。这是如何做:

StringBuilder builder = new StringBuilder(format.Length + (args.Length * 8));

因此,对于每个传递的参数,它保留8个字符。如果参数是一个数字值,那么太糟糕了,我们浪费了一些空间。如果参数是在上返回一些长文本的自定义对象ToString(),则可能甚至需要重新分配(当然,最坏的情况)。

与此相比,串联只会浪费对象数组的空间(考虑到它是引用数组,不会浪费太多)。没有针对格式说明符的解析,也没有中介StringBuilder。这两种方法都存在装箱/拆箱的开销。

我选择String.Format的唯一原因是涉及本地化时。将格式字符串放入资源中可让您支持不同的语言而不会弄乱代码(请考虑一下格式化值根据语言而改变顺序的情况,即“ {0}小时和{1}分钟之后”在日语中可能看起来完全不同: )。


总结一下我的第一篇文章(而且很长):

  • 对我而言,最好的方法(就性能与可维护性/可读性而言)是使用字符串连接,而无需任何ToString()调用
  • 如果您追求出色的表现,ToString()请自己拨打电话以避免拳击(我在可读性方面有些偏向)-与问题中的第一个选项相同
  • 如果您要向用户显示本地化的字符串(此处不是这种情况),String.Format()则具有优势。

5
1)string.Format使用ReSharper时“安全”;也就是说,它与[错误使用]的任何其他代码一样安全。2)string.Format 确实允许“安全” nullstring.Format("A{0}B", (string)null)结果为“ AB”。3)我很少关心这种性能水平(为此,我很少抽出一天StringBuilder)……

同意2),我将编辑帖子。无法验证从1.1开始是否安全,但是最新的框架确实是空安全的。
Dan C.

如果操作数之一是带有返回值的方法调用而不是参数或变量,是否仍使用string.Concat?
理查德·科莱特

2
@RichardCollette是的,即使您连接方法调用的返回值(例如,string s = "This " + MyMethod(arg) + " is a test";将其编译为String.Concat()发布模式的调用),也使用String.Concat 。
Dan C.

很棒的答案;很好的书面和解释。
Frank V

6

我认为第一种选择更具可读性,这应该是您的主要关注点。

xlsSheet.Write("C" + rowIndex.ToString(), null, title);

string.Format在后台使用StringBuilder(请使用反射器检查),因此除非您进行了大量的串联操作,否则它不会带来任何性能优势。这对于您的情况而言会比较慢,但现实情况是,这种微性能优化决策在大多数情况下都是不合适的,除非您处于循环之中,否则您应该真正专注于代码的可读性。

无论哪种方式,如果您确实认为自己有性能问题,请先编写可读性报告,然后使用性能分析器识别热点。



5

对于一个简单的单连接的简单情况,我觉得这不值得string.Format(而且我还没有进行测试,但是我怀疑对于像这样的简单情况,它string.Format 可能会稍微慢一些,使用格式字符串解析会怎样)和所有)。像乔恩·斯凯特(Jon Skeet)一样,我宁愿不显式调用.ToString(),因为这将由string.Concat(string, object)重载,并且我认为代码看上去更易于阅读。

但是对于多个连接(主观几个),我绝对更喜欢string.Format。在某种程度上,我认为级联不必要地影响了可读性和性能。

如果格式字符串有很多参数(同样,“许多”是主观的),我通常更喜欢在替换参数中包含带注释的索引,以免丢失对哪个参数值的跟踪。一个人为的例子:

Console.WriteLine(
    "Dear {0} {1},\n\n" +

    "Our records indicate that your {2}, \"{3}\", is due for {4} {5} shots.\n" +
    "Please call our office at 1-900-382-5633 to make an appointment.\n\n" +

    "Thank you,\n" +
    "Eastern Veterinary",

    /*0*/client.Title,
    /*1*/client.LastName,
    /*2*/client.Pet.Animal,
    /*3*/client.Pet.Name,
    /*4*/client.Pet.Gender == Gender.Male ? "his" : "her",
    /*5*/client.Pet.Schedule[0]
);

更新资料

在我看来,我给出的示例有点令人困惑,因为看来我在这里同时使用串联string.Format。是的,在逻辑上和词汇上,这就是我所做的。但是串联将全部由编译器1进行优化,因为它们都是字符串文字。因此,在运行时,将只有一个字符串。因此,我想应该说我宁愿在运行时避免许多串联。

当然,除非您仍然使用C#5或更早版本,否则本主题的大部分内容已过时。现在,我们已经插入了字符串,出于可读性考虑,字符串string.Format几乎在所有情况下都优于。如今,除非我将值直接直接连接到字符串文字的开头或结尾,否则我几乎总是使用字符串插值。今天,我将这样编写我的早期示例:

Console.WriteLine(
    $"Dear {client.Title} {client.LastName},\n\n" +

    $"Our records indicate that your {client.Pet.Animal}, \"{client.Pet.Name}\", " +
    $"is due for {(client.Pet.Gender == Gender.Male ? "his" : "her")} " +
    $"{client.Pet.Schedule[0]} shots.\n" +
    "Please call our office at 1-900-382-5633 to make an appointment.\n\n" +

    "Thank you,\n" +
    "Eastern Veterinary"
);

这样您确实会丢失编译时级联。 每个内插字符串string.Format都由编译器转换为对它们的调用,并且它们的结果在运行时连接在一起。这意味着为了牺牲可读性而牺牲了运行时性能。在大多数情况下,这是一个值得的牺牲,因为运行时的损失可以忽略不计。但是,在性能关键的代码中,您可能需要分析不同的解决方案。


1 您可以在C#规范中看到这一点:

...在常量表达式中允许以下构造:

...

  • 预定义的+ ...二进制运算符...

您还可以通过一些代码来验证它:

const string s =
    "This compiles successfully, " +
    "and you can see that it will " +
    "all be one string (named `s`) " +
    "at run time";

1
仅供参考,您可以使用@“ ... multi line string”代替所有串联。
亚伦·帕尔默

是的,但是您必须左对齐字符串。@字符串在引号之间包含所有新行和制表符。
P爸爸

我知道这很旧,但是在这种情况下,我会说将格式字符串放在resx文件中。
安迪2012年

2
哇,每个人都专注于字符串文字,而不是问题的核心。
P Daddy

heheh-我只是注意到您内部的String串联String.Format()
Kristopher

3

如果您的字符串比较复杂,并且串联了许多变量,那么我将选择string.Format()。但是对于您的情况而言,字符串的大小和要连接的变量的数量,我会选择您的第一个版本,它更简单


3

我看过String.Format(使用Reflector),它实际上创建了一个StringBuilder,然后在其上调用AppendFormat。因此,对于多个搅拌,它比concat更快。最快(我相信)是创建一个StringBuilder并手动执行Append的调用。当然,“很多”的数目尚待猜测。我会使用+(实际上,因为我主要是VB程序员)来完成一些与您的示例一样简单的操作。随着变得越来越复杂,我使用了String.Format。如果有很多变量,那么我将使用StringBuilder和Append,例如,我们有构建代码的代码,在这里我使用一行实际代码输出一行生成的代码。

关于为这些操作中的每个操作创建多少个字符串似乎有些猜测,因此让我们举几个简单的例子。

"C" + rowIndex.ToString();

“ C”已经是一个字符串。
rowIndex.ToString()创建另一个字符串。(@manohard-不会对rowIndex进行装箱),
然后得到最后一个字符串。
如果我们以

String.Format("C(0)",rowIndex);

那么我们将“ C {0}”作为字符串
rowIndex装箱以传递给函数
创建一个新的
stringbuilder会在字符串生成器上调用AppendFormat-我不知道有关AppendFormat函数的详细信息,但假设它是超高效,仍然必须将装箱的rowIndex转换为字符串。
然后将stringbuilder转换为新字符串。
我知道StringBuilders试图防止发生无意义的内存复制,但是与普通的串联相比,String.Format最终仍会产生额外的开销。

如果我们现在以更多字符串为例

"a" + rowIndex.ToString() + "b" + colIndex.ToString() + "c" + zIndex.ToString();

我们有6个字符串开头,在所有情况下都相同。
使用串联,我们还有4个中间字符串加上最终结果。通过使用String,Format(或StringBuilder)消除了那些中间结果。
请记住,要创建每个中间字符串,必须将前一个字符串复制到新的内存位置,而不仅仅是内存分配可能会变慢。


4
Nitpick。在“ a” + ... +“ b” + ... +“ c” + ...中,实际上没有4个中间字符串。编译器将生成对String.Concat(params string [] values)静态方法的调用,并且它们将全部立即串联在一起。不过,出于可读性考虑,我还是更喜欢string.Format。
P爸爸

2

我喜欢String.Format,因为它可以使格式化的文本比内联串联更易于阅读和阅读,它的灵活性也更高,允许您格式化参数,但是对于像您这样的短期使用,我认为串联没有问题。

对于循环内或大字符串中的串联,应始终尝试使用StringBuilder类。


2

该示例可能太琐碎,以至于看不到差异。实际上,我认为在大多数情况下,编译器可以完全消除任何差异。

但是,如果我不得不猜测,我会string.Format()为更复杂的场景提供优势。但这更多是一种直觉,那就是利用缓冲区而不是生成多个不可变的字符串,而不是基于任何实际数据,可能会做得更好。


1

我同意以上几点,我认为应该提到的另一点是代码的可维护性。string.Format允许更轻松地更改代码。

即我有一条消息 "The user is not authorized for location " + location"The User is not authorized for location {0}"

如果我想更改消息说: location + " does not allow this User Access""{0} does not allow this User Access"

使用string.Format我要做的就是更改字符串。对于串联,我必须修改该消息

如果在多个地方使用,可以节省时间。


1

我的印象是string.format更快,在此测试中似乎慢了3倍

string concat = "";
        System.Diagnostics.Stopwatch sw1 = new System.Diagnostics.Stopwatch    ();
        sw1.Start();
        for (int i = 0; i < 10000000; i++)
        {
            concat = string.Format("{0}{1}{2}{3}{4}{5}{6}{7}{8}{9}{10}","1", "2" , "3" , "4" , "5" , "6" , "7" , "8" , "9" , "10" , i);
        }
        sw1.Stop();
        Response.Write("format: "  + sw1.ElapsedMilliseconds.ToString());
        System.Diagnostics.Stopwatch sw2 = new System.Diagnostics.Stopwatch();
        sw2.Start();
        for (int i = 0; i < 10000000; i++)
        {
            concat = "1" + "2" + "3" + "4" + "5" + "6" + "7" + "8" + "9" + "10" + i;
        }
        sw2.Stop();

string.format用了4.6秒,而使用'+'则用了1.6秒。


7
编译器将其识别"1" + "2" + "3" + "4" + "5" + "6" + "7" + "8" + "9" + "10"为一个字符串文字,因此该行实际上变得比上一行"12345678910" + i更快string.Format(...)
wertzui 2015年


0

我对各种字符串方法进行了一些分析,包括string.Format,StringBuilder和字符串连接。字符串连接几乎总是优于其他构建字符串的方法。因此,如果性能是关键,那么它会更好。但是,如果性能不是很关键,那么我个人会发现string.Format易于在代码中遵循。(但这是一个主观原因)但是,StringBuilder在内存利用率方面可能是最有效的。



-1

与String.Format相比,字符串串联需要更多的内存。因此,连接字符串的最佳方法是使用String.Format或System.Text.StringBuilder对象。

让我们以第一种情况为例:“ C” + rowIndex.ToString()假设rowIndex是一个值类型,因此ToString()方法必须Box将该值转换为String,然后CLR为包含两个值的新字符串创建内存。

如果string.Format需要对象参数,并将rowIndex作为对象并内部将其转换为字符串,那么Boxing就是它的固有功能,并且不会像第一种情况那样占用太多内存。

对于短字符串,我猜并没有那么重要...

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.