“请对我的代码指手划脚”是我们群内搞的一个不定期的常规性活动,以代码审阅和细节重构
为主线,大家可以自由发表自己的意见和建议,也算得上是一种思维风暴。感觉到这个活动很有意义,有必要总结并记录下来。今天我发起了4短代码,都有一定的
代表性。今天我就其中的一个代码片段的重构做一个简单的总结和分享。
首先我们看看目标代码:
1 public static string TestA(List<
string>
items)
2 {
3 var builder =
new StringBuilder(
);
4
5 foreach (
var item
in items)
6 {
7 if (builder.Length >
0)
8 {
9 builder.Append(
"|");
10 builder.Append(item);
11 }
12 else builder.Append(item);
13 }
14
15 return builder.ToString();
16 }
这里我使用C#来做示例,实际上,语言是相通的,我们将要谈论的优化技巧在大多数编程环境中都是通用的。针对如上代码,总计收集到了如下优化建议。
建议一:代码重用性
我们可以看到,if...else...子句中有一段“builder.Append(item);”代码是重复的,改变流程可以让它们只出现一次,重构后结果如下:
1 foreach (var item in items)
2 {
3 if (builder.Length > 0) builder.Append("|"); // 去掉了大括号
4
5 builder.Append(item);
6 }
建议二:性能优化
我们知道StringBuilder类的构造函数中有一个capacity参数,这个参数意味着StringBuilder对象初始化时预分配的内
存大小。如果能够适当的设定一个值,那么对提升性能应该会很有帮助。因为这可以减少内存分配的次数,StringBuilder默认情况下是以2的N次方
的形式不断翻倍来调整内存需求的(对于我们来说,这个过程是自动的)。
建议三:还是性能优化
建议将foreach拆分为一次手工Append和一个for循环,如此可以避免在foreach内部的if判断。在数据量大的时候可以大大的减少CPU的时钟周期的占用,这个建议很不错!重构后代码如下:
1 public static string TestA(List<
string>
items)
2 {
3 // 这里是个capacity优化的假设值,实际运行中需要不断的测试调优
4 var builder =
new StringBuilder(
100000);
5
6 builder.Append(items[
0]);
7
8 for (
var i =
1; i < items.Count; i++
)
9 {
10 var item =
items[i];
11
12 builder.Append(
"|");
13 builder.Append(item);
14 }
15
16 return builder.ToString();
17 }
建议四:内存优化
其实是综合了建议二和建议三,如下:
1 public static string TestA(List<
string>
items)
2 {
3 var length = items.Sum(t =>
t.Length);
4
5 length += (items.Count -
1);
6
7 if (length ==
0)
return String.Empty;
8
9 // 先计算出capacity的值
10 var builder =
new StringBuilder(length);
11
12 builder.Append(items[
0]);
13
14 for (
var i =
1; i < items.Count; i++
)
15 {
16 builder.Append(
"|");
17 builder.Append(items[i]);
// 消灭了之前的一个局部变量,减少内存分配
18 }
19
20 return builder.ToString();
21 }
我的答复
其实,我出这个题目并不是为了考察性能优化、内存优化等问题,不过猴子们能想出各种招数来解题,我真的很欣慰!至少大家都在参与,都在动脑筋!这是好事!
通读这篇文章之后,我相信您已经发现了题目原本的业务逻辑是想把一个string集合中的字符串使用“|”字符串联起来,而且不能在结果字符串的两边出现“|”。因此,我期望能有童鞋想出如下的重构建议:
1 public static string TestB(List<string> items)
2 {
3 return String.Join("|", items);
4 }
您会不会感觉到我这么说很坑爹呢?
是的!代码的细节重构不仅仅是各种优化和代码的写法、编码体验、编码规范等,还有个重要的地方就是业务逻辑!编程是什么?编程是处理数据的手段和过程,同样的结果可能会有很多途径抵达,对我们来说,要从这些途径中挑选出最简单易用的,性能差异不要太大就可以了。
性能测试往往具有很强的随机性,所以我们的测试必须要在不同的数量级下反复测试多遍,然后收集一个平均结果(最好是去掉最大值和最小值)来对比。至于性能差异的大小,在同一个数量级之内的,我们都认为“差异不大”或“没有差异”,超出两个数量级的时候一定要警惕!
本文附带了我编写的测试代码,大家可以下载后运行对比一下。我随机选择了一个测试结果供大家参考:
测试数据准备完成,请按任意键继续……
itemCount StringBuilder String.Join
1 1.851500 0.318600
10 0.027500 0.064400
100 0.225500 0.261600
1000 10.104700 2.324100
10000 19.039900 20.094800
100000 216.185100 251.624600
1000000 2364.580300 3401.948900
10000000 22862.921600 33593.679800
测试完毕!
可以看出,我们的建议四和String.Join方法的性能差异其实很小,可以忽略不计,通常我们谁会去处理一个几百万上千万这么大的集合呢?
至于这两种方法为什么差异不大,其实,我们只需要看看String.Join方法的实现就知道了,通过 .NET Reflector反编译后我们发现它的实现也使用了类似于建议四的方案:
1 [SecuritySafeCritical]
2 public static unsafe string Join(
string separator,
string[] value,
int startIndex,
int count)
3 {
4 if (value ==
null)
throw new ArgumentNullException(
"value");
5 if (startIndex <
0)
throw new ArgumentOutOfRangeException(
"startIndex", Environment.GetResourceString(
"ArgumentOutOfRange_StartIndex"));
6 if (count <
0)
throw new ArgumentOutOfRangeException(
"count", Environment.GetResourceString(
"ArgumentOutOfRange_NegativeCount"));
7 if (startIndex > (value.Length - count))
throw new ArgumentOutOfRangeException(
"startIndex", Environment.GetResourceString(
"ArgumentOutOfRange_IndexCountBuffer"));
8 if (separator ==
null) separator =
Empty;
9 if (count ==
0)
return Empty;
10
11 var length =
0;
12 var num2 = (startIndex + count) -
1;
13
14 for (
var i = startIndex; i <= num2; i++)
if (value[i] !=
null) length +=
value[i].Length;
15
16 length += (count -
1)*
separator.Length;
17
18 if ((length <
0) || ((length +
1) <
0))
throw new OutOfMemoryException();
19 if (length ==
0)
return Empty;
20
21 string str =
FastAllocateString(length);
22
23 fixed (
char* chRef = &
str.m_firstChar)
24 {
25 var buffer =
new UnSafeCharBuffer(chRef, length);
26 buffer.AppendString(value[startIndex]);
27
28 for (
var j = startIndex +
1; j <= num2; j++
)
29 {
30 buffer.AppendString(separator);
31 buffer.AppendString(value[j]);
32 }
33 }
34
35 return str;
36 }
代码就不再解读了,大家可以慢慢体会。
本文想告诉大家的是:代码细节重构不要只停留在语言表面上,深入业务逻辑有时候会得到意想不到的结果!
代码下载:
代码细节重构-String.Join.zip