简介
你在代码中处理C#字符串的方法可能会对性能产生令人吃惊的影响。程序中需要考虑两个由于使用字符串而产生的问题:临时字符串变量的使用和字符串连接。
背景知识
1.String是引用类型,在堆上分配内存。
2.String运算时会产生一个新的实例。当看到 myString.ToUpper() 时,我经常都会忘记它并不是改变 myString 的内容而是返回一整个全新的字符串(这是由于C# 字符串是不可变的)。
3.String 对象一旦生成不可改变(Immutable)。
4.定义相等运算符(== 和 !=)是为了比较 String 对象(而不是引用)的值。
String Comparison and Temporary String Creation
下面的例程是一个蹩脚的非大小写敏感的字符串比较。用于比较的例程的代码是:
- static bool BadCompare(string stringA, string stringB)
- {
- return (stringA.ToUpper() == stringB.ToUpper());
- }
对于这段代码,FxCop 给出如下的建议:
这项建议的意思是每次对 ToUpper() 的调用都会创造一个临时字符串,而这个临时字符串是由垃圾收集器来创建和管理的。这需要额外的时间和使用更多的内存。 String.Compare 方法(相对来说)更加高效。
String Concatenation inside a loop
***那对测试例程设想字符串的连接是在一个循环里面进行的。
“蹩脚”的测试例程的代码如下:
- static string BadConcatenate(string [] items)
- {
- string strRet = string .Empty;
- foreach (string item in items)
- {
- strRet += item;
- }
- return strRet;
- }
当 FxCop 看到这段代码,它就会很愤怒,甚至用红色标记这项被破的规条! FxCop 这样说道:
“优良”的测试例程的代码如下:
- static string GoodConcatenate(string [] items)
- {
- System.Text.StringBuilder builder = new System.Text.StringBuilder();
- foreach (string item in items)
- {
- builder.Append(item);
- }
- return builder.ToString();
- }
这段代码几乎被用作展示 System.Text.StringBuilder 的用法的***例子。蹩脚的代码的问题是创建了过多的临时字符串。由于字符串的不可变特性,连接操作符(+=)实际上用原来那两个字符串来创建一个新的字符串,然后把原来的字符串实例指向这个新的字符串。
但是,依据 nprof 来研究代码性能,我们发现运行 BadConcatenate 只需总执行时间的 5.67% ,而 GoodConcatenate 则是 22.09% 。也就是说:
使用 StringBuilder 耗费的时间几乎是简单的字符串连接的四倍!
为什么呢?
部分原因在于这个测试的设计——连接例程仅仅连接了十个简短的字符串。 StringBuilder 是一个比简单的不可变的字符串类更复杂的类,因此创建一个 StringBuilder 比起进行十个简单的字符串连接在性能上是昂贵很多的。
我重复地做不同数目的字符串连接的测试,并且发现以下结果:
结论
使用 String.Compare 方法进行非大小写敏感的C#字符串比较。这样更快。而且代码优雅和简单。
仅当你在一个循环里进行超过 600 次的字符串连接时,使用 StringBuilder 来获得更好的速度。这里需要提醒的是,你所处理的字符串的长度也会影响最终的速度,同样会影响垃圾收集器的效果,所以你应该根据你实际的代码具体问题具体分析
使用 StringBuilder 来处理字符串的连接应该是绝大多数 .NET 开发人员的共识了。但你有否曾经怀疑过这一经验原则的适用性是否真如想象中那么广泛呢?读过本文后,或许你已经意识到这是个适度的问题。对小规模的字符串连接使用 StringBuilder 所带来的改善根本不足以抵偿因 StringBuilder 本身的复杂性所产生的开销;只有当连接规模达到临界规模,两者才能相互抵偿从而达至平衡。
【编辑推荐】