【发布时间】:2008-11-17 21:20:32
【问题描述】:
我有一小段代码正在解析索引值以确定 Excel 中的单元格输入。这让我思考...
有什么区别
xlsSheet.Write("C" + rowIndex.ToString(), null, title);
和
xlsSheet.Write(string.Format("C{0}", rowIndex), null, title);
一个比另一个“更好”吗?为什么?
【问题讨论】:
我有一小段代码正在解析索引值以确定 Excel 中的单元格输入。这让我思考...
有什么区别
xlsSheet.Write("C" + rowIndex.ToString(), null, title);
和
xlsSheet.Write(string.Format("C{0}", rowIndex), null, title);
一个比另一个“更好”吗?为什么?
【问题讨论】:
我最初的偏好(来自 C++ 背景)是 String.Format。由于以下原因,我后来放弃了它:
- 字符串连接允许空值,String.Format 不允许。写入“s1 + null + s2”不会中断,它只是将 null 值视为 String.Empty。好吧,这可能取决于您的具体情况 - 在某些情况下,您希望出现错误而不是默默地忽略 null FirstName。然而,即使在这种情况下,我个人更喜欢自己检查空值并抛出特定错误,而不是从 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 在做什么——你会惊讶于处理格式字符串所需的代码量。最重要的是(我已经看到有关内存消耗的 cmets),String.Format 在内部使用了 StringBuilder。方法如下:
StringBuilder builder = new StringBuilder(format.Length + (args.Length * 8));
所以对于每个传递的参数,它保留 8 个字符。如果参数是一位数,那么太糟糕了,我们浪费了一些空间。如果参数是在ToString() 上返回一些长文本的自定义对象,那么甚至可能需要重新分配(当然是最坏的情况)。
与此相比,串联只会浪费对象数组的空间(不算太多,考虑到它是一个引用数组)。没有格式说明符的解析,也没有中间的 StringBuilder。两种方法都存在装箱/拆箱开销。
我选择 String.Format 的唯一原因是涉及本地化。将格式字符串放入资源中可以让您支持不同的语言而不会弄乱代码(想想格式化值根据语言改变顺序的场景,即“在 {0} 小时和 {1} 分钟之后”在日语中可能看起来完全不同: )。
总结一下我的第一篇(而且很长)的帖子:
ToString() 调用ToString() 以避免装箱(我对可读性有些偏向) - 与您问题中的第一个选项相同String.Format() 有优势。【讨论】:
string.Format 在使用 ReSharper 时是“安全的”;也就是说,它与任何其他可以[错误地]使用的代码一样安全。 2) string.Format 确实 允许“安全”null:string.Format("A{0}B", (string)null) 导致“AB”。 3) 我很少关心这种级别的性能(为此,我退出StringBuilder 是罕见的一天)...
string s = "This " + MyMethod(arg) + " is a test"; 在发布模式下编译为 String.Concat() 调用。
在 C# 6 之前
说实话,我认为第一个版本更简单 - 尽管我会将其简化为:
xlsSheet.Write("C" + rowIndex, null, title);
我怀疑其他答案可能谈论性能损失,但老实说,这将是最小的如果存在的话 - 这个串联版本不需要解析格式字符串。
格式字符串非常适合用于本地化等目的,但在这种情况下,连接更简单并且效果也很好。
使用 C# 6
字符串插值使很多东西在 C# 6 中更易于阅读。在这种情况下,您的第二个代码变为:
xlsSheet.Write($"C{rowIndex}", null, title);
这可能是最好的选择,IMO。
【讨论】:
xlsSheet.Write($"C{rowIndex}", null, title);
我认为第一个选项更具可读性,这应该是您最关心的问题。
xlsSheet.Write("C" + rowIndex.ToString(), null, title);
string.Format 在后台使用 StringBuilder(请查看 reflector),因此除非您进行大量连接,否则它不会有任何性能优势。对于您的场景来说它会比较慢,但现实情况是这种微观性能优化决策在大多数情况下是不合适的,除非您处于循环中,否则您应该真正关注代码的可读性。
无论哪种方式,如果您确实认为自己有性能问题,请先编写可读性,然后使用performance profiler 来识别您的热点。
【讨论】:
对于一个简单的单一连接的简单案例,我觉得不值得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]
);
我觉得我给出的例子有点令人困惑,因为我似乎在这里使用了 both 连接和string.Format。是的,从逻辑和词汇上讲,这就是我所做的。但是编译器1 会优化所有连接,因为它们都是字符串文字。所以在运行时,会有一个字符串。所以我想我应该说我更喜欢在运行时避免许多串联。
当然,这个主题的大部分内容现在已经过时了,除非您仍然坚持使用 C# 5 或更早版本。现在我们有了interpolated strings,就可读性而言,几乎在所有情况下都远远优于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 你可以在the C# specification看到这个:
...在常量表达式中允许以下结构:
...
- 预定义的 + ... 二元运算符...
你也可以用一点代码来验证:
const string s =
"This compiles successfully, " +
"and you can see that it will " +
"all be one string (named `s`) " +
"at run time";
【讨论】:
String.Format()中的字符串连接
如果你的字符串更复杂,有很多变量被连接,那么我会选择 string.Format()。但是对于在您的情况下要连接的字符串的大小和变量的数量,我会使用您的第一个版本,它更多的是spartan。
【讨论】:
这个例子可能太微不足道了,无法注意到差异。事实上,我认为在大多数情况下,编译器可以优化掉任何差异。
但是,如果我不得不猜测,我会给string.Format() 一个优势,以应对更复杂的场景。但这更像是一种直觉,它可能会更好地利用缓冲区而不是生成多个不可变字符串,而不是基于任何真实数据。
【讨论】:
我查看了 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 被装箱以传递给函数
创建了一个新的字符串生成器
在字符串生成器上调用 AppendFormat - 我不知道 AppendFormat 函数的详细信息,但假设它非常高效,它仍然必须将装箱的 rowIndex 转换为字符串。
然后将字符串生成器转换为新字符串。
我知道 StringBuilders 试图防止发生无意义的内存复制,但与普通连接相比,String.Format 仍然会产生额外的开销。
如果我们现在举几个字符串的例子
"a" + rowIndex.ToString() + "b" + colIndex.ToString() + "c" + zIndex.ToString();
我们从 6 个字符串开始,这对于所有情况都是相同的。
使用连接我们还有 4 个中间字符串加上最终结果。正是那些通过使用 String、Format(或 StringBuilder)消除的中间结果。
请记住,要创建每个中间字符串,必须将前一个字符串复制到新的内存位置,不仅仅是内存分配可能很慢。
【讨论】:
我喜欢 String.Format 因为它可以让你的格式化文本比内联连接更容易理解和阅读,而且它更灵活允许你格式化你的参数,但是对于像你这样的短期用途,我认为连接没有问题。
对于循环内或大字符串中的连接,您应该始终尝试使用 StringBuilder 类。
【讨论】:
我同意上面的很多观点,我认为应该提到的另一点是代码可维护性。 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 我所要做的就是更改字符串。 对于连接,我必须修改该消息
如果在多个地方使用可以节省分配的时间。
【讨论】:
我的印象是 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 秒。
【讨论】:
"1" + "2" + "3" + "4" + "5" + "6" + "7" + "8" + "9" + "10"识别为一个字符串文字,因此该行有效地变为"12345678910" + i,这比之前的string.Format(...)更快
当格式模板(“C{0}”)存储在配置文件(如 Web.config / App.config)中时,string.Format 可能是更好的选择
【讨论】:
我对各种字符串方法进行了一些分析,包括 string.Format、StringBuilder 和字符串连接。字符串连接几乎总是优于其他构建字符串的方法。因此,如果性能是关键,那么它会更好。但是,如果性能不重要,那么我个人认为 string.Format 更容易在代码中遵循。 (但这是一个主观原因)然而,StringBuilder 可能在内存利用率方面是最有效的。
【讨论】:
在性能方面我更喜欢 String.Format
【讨论】:
void Main()
{
var start = CurrentTimeMillis();
for (var i = 0; i < 1000000; i++)
{
var s = "Hi " + i.ToString() + "; Hi to you " + (i * 2).ToString();
}
var end = CurrentTimeMillis();
Console.WriteLine("Concatenation = " + ((end - start)).ToString() + " millisecond");
start = CurrentTimeMillis();
for (var i = 0; i < 1000000; i++)
{
var s = String.Format("Hi {0}; Hi to you {1}", i, +i * 2);
}
end = CurrentTimeMillis();
Console.WriteLine("Format = " + ((end - start)).ToString() + " millisecond");
start = CurrentTimeMillis();
for (var i = 0; i < 1000000; i++)
{
var s = String.Concat("Hi ", i.ToString(), "; Hi to you ", (i * 2).ToString());
}
end = CurrentTimeMillis();
Console.WriteLine("Strng.concat = " + ((end - start)).ToString() + " millisecond");
start = CurrentTimeMillis();
for (int i = 0; i < 1000000; i++)
{
StringBuilder bldString = new StringBuilder("Hi ");
bldString.Append(i).Append("; Hi to you ").Append(i * 2);
}
end = CurrentTimeMillis();
Console.WriteLine("String Builder = " + ((end - start)) + " millisecond");
}
private static readonly DateTime Jan1st1970 = new DateTime
(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
public static long CurrentTimeMillis()
{
return (long)(DateTime.UtcNow - Jan1st1970).TotalMilliseconds;
}
Concatenation = 69 millisecond
Format = 142 millisecond
Strng.concat = 62 millisecond
String Builder = 91 millisecond
为了进行公平的比较,我实例化了一个新的 StringBuilder 而不是在循环之外创建的(由于循环追加在一个构建器的重新分配空间末尾,这可能会更快)。
【讨论】:
与 String.Format 相比,字符串连接占用更多内存。所以连接字符串的最佳方法是使用 String.Format 或 System.Text.StringBuilder 对象。
我们来看第一种情况:"C" + rowIndex.ToString() 假设 rowIndex 是一个值类型,因此 ToString() 方法必须通过 Box 将值转换为 String,然后 CLR 为包含两个值的新字符串创建内存。
当 string.Format 需要对象参数并将 rowIndex 作为对象并在内部将其转换为字符串时,当然会有 Boxing,但它是内在的,而且它不会像第一种情况那样占用太多的内存。
对于短字符串,我猜这并不重要......
【讨论】: