使用string.Format需要注意的一個性能問題


今天,我在寫C#代碼時,突然發現一個最熟悉的陌生人 —— string.Format。在寫C#代碼的日子里,與它朝夕相伴,卻沒有真正去了解它。只知道在字符串比較多時,用它比用加號進行字符串連接效率更高(當然也更方便)。可是卻從來沒有問過為什么?

在生活中也有類似的現象,與你朝夕相處、你最熟悉的人,你往往不會進一步去了解她(他),你已經習慣了她(他),你認為你已經太了解她(他)了。。。真的是這樣嗎?這值得去思考。。。

博問中的一個問題 —— StringBuilder,String.concat(),String+String 哪一個效率高?  激發了我的好奇心,想一探string.Format的究竟,而且在開發中也正好遇到一個字符串連接的問題。

了解.NET世界中的東西其實很簡單,只要通過工具反編譯出相應的.NET類庫代碼,我們來看看string.Fomat的代碼:

    public static string Format(string format, object arg0, object arg1, object arg2)
    {
      if (format == null)
        throw new ArgumentNullException("format");
      return string.Format((IFormatProvider) null, format, arg0, arg1, arg2);
    }

實際調用的是另外一個簽名的string.Format:

    public static string Format(IFormatProvider provider, string format, params object[] args)
    {
      if (format == null || args == null)
        throw new ArgumentNullException(format == null ? "format" : "args");
      StringBuilder stringBuilder = new StringBuilder(format.Length + args.Length * 8);
      stringBuilder.AppendFormat(provider, format, args);
      return ((object) stringBuilder).ToString();
    }

哦,原來用的就是StringBuilder(也許你早就知道了),string.Format只是StringBuilder的改裝精簡版。

既然是StringBuilder,它必然無法避免一個影響StringBuilder性能的問題 —— 初始化容量(capacity)的問題,string.Format是如何解決的呢?從上面的代碼一眼就可以看出,初始化容量是這么計算出來的:

format.Length + args.Length * 8

從這個計算公式可以看出,假設需要format的字符串是10個,如果這10字符串累加起來的字符數不超過80,就能發揮StringBuilder的最佳性能;否則,StringBuider需要擴容,從而帶來性能損失。

所以,對於大字符串,string.Format不是最佳選擇。

那最佳選擇是什么?還是StringBuilder,只不過要自己寫代碼計算初始化容量。分享一下今天我們在實際開發中使用的代碼:

var bodyFormat = "<span id=\"comment_body_{0}\">{1}</span><br/>";                        
var diggFormat = " <a href=\"javascript:void(0);\" onclick=\"voteComment({0},'Digg')\">支持({2})</a>";
var buryFormat = " <a href=\"javascript:void(0);\" onclick=\"voteComment({0},'Bury')\">反對({3})</a>";
var args = new string[]{ comment.ID.ToString(), comment.Body, comment.DiggCount.ToString(), comment.BuryCount.ToString() };
//計算初始化容量
int capacity = bodyFormat.Length + diggFormat.Length + buryFormat.Length;
for (int i = 0; i < args.Length; i++)
{
    capacity += args[i].Length;
}
var sb = new StringBuilder(capacity);
sb.AppendFormat(bodyFormat,args); 
sb.AppendFormat(diggFormat,args); 
sb.AppendFormat(buryFormat,args);
Post.Text = sb.ToString();

這里沒有使用string.Format,一是因為comment.Body的字符數會很多,string.Format分配的初始化容量不夠。二是因為string.Format不能分批Fomat,格式字符串只能寫在一起,造成格式字符串很長,也就是bodyFormat, diggFormat, buryFormat要拼成一個字符串。

麻煩主要在參數字符串(args)的長度計算,要將每個字符串的字符數進行累加。我們采用的方法是將所有參數放在string[]類型的變量中,通過遍歷數組進行計算,然后將這個string[]類型的變量直接傳給StringBuilder.AppendFormat(它支持的參數類型是object[])。

小結

寫這篇博文不是為讓你棄用string.Format,而是讓你了解它所存在的限制,在某些性能要求極高的場景下,可以考慮到這個影響因素。

更新

針對這個問題,實現了兩個擴展方法。

1. 針對單個格式字符串

namespace System
{
    public static class StringExtension
    {
        public static string FormatWith(this string format, params object[] args)
        {
            if (format == null || args == null)
            {
                throw new ArgumentNullException((format == null) ? "format" : "args");
            }
            else
            {
                var capacity = format.Length + args.Where(a => a != null).Select(p => p.ToString()).Sum(p => p.Length);
                Console.WriteLine(capacity);
                var stringBuilder = new StringBuilder(capacity);
                stringBuilder.AppendFormat(format, args);
                return stringBuilder.ToString();
            }
        }        
    }
}

調用示例:

"welcome to {0}! welcome to {1}!".FormatWith("www.cnblogs.com", "q.cnblogs.com");

2. 針對多個格式字符串

namespace System
{
    public static class StringExtension
    {
        public static string FormatWith(this IEnumerable<string> formats, params object[] args)
        {
            if (formats == null || args == null)
            {
                throw new ArgumentNullException((formats == null) ? "formats" : "args");
            }
            else
            {
                var capacity = formats.Where(f => !string.IsNullOrEmpty(f)).Sum(f => f.Length) +
                    args.Where(a => a != null).Select(p => p.ToString()).Sum(p => p.Length);
                var stringBuilder = new StringBuilder(capacity);
                foreach (var f in formats)
                {
                    if (!string.IsNullOrEmpty(f))
                    {
                        stringBuilder.AppendFormat(f, args);
                    }
                }
                return stringBuilder.ToString();
            }
        }
    }
}

調用示例:

new string[] { "welcome to {0}!", " welcome to {1}!" }.FormatWith("www.cnblogs.com", "q.cnblogs.com");

前面使用StringBuilder的代碼改為調用擴展方法:

Post.Text = new string[]{
"<span id=\"comment_body_{0}\" class=\"blog_comment_body\">{1}</span><br/>",
" <a href=\"javascript:void(0);\" class=\"comment_vote\" onclick=\"voteComment({0},'Digg')\">支持({2})</a>",
" <a href=\"javascript:void(0);\" class=\"comment_vote\" onclick=\"voteComment({0},'Bury')\">反對({3})</a>"
}.FormatWith(comment.ID, comment.Body, comment.DiggCount, comment.BuryCount);


免責聲明!

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



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