今天,我在寫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);