代碼細節重構:請對我的代碼指手划腳(一)


“請對我的代碼指手划腳”是我們群內搞的一個不定期的常規性活動,以代碼審閱和細節重構為主線,大家可以自由發表自己的意見和建議,也算得上是一種思維風暴。感覺到這個活動很有意義,有必要總結並記錄下來。今天我發起了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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM