字符串连接,常用的三种方式:StringBuilder、+、string.Format。很多人认为StringBuilder的效率高于+,这种观念是不正确的。
一般来说,对于数量固定的字符串连接,+的效率是最高的。比如:
string sql = "update tableName set int1=" + int1.ToString() + ",int2=" + int2.ToString() + ",int3=" + int3.ToString() + " where id=" + id.ToString();
编译器会优化为
string sql = string.Concat(new string[] { "update tableName set int1=", int1.ToString(), ",int2=", int2.ToString(), ",int3=", int3.ToString(), " where id=", id.ToString() });
再来看看string.Concat是怎么实现的
public static string Concat(params string[] values) { int totalLength = 0; if (values == null) { throw new ArgumentNullException("values"); } string[] strArray = new string[values.Length]; for (int i = 0; i < values.Length; i++) { string str = values[i]; strArray[i] = (str == null) ? Empty : str; totalLength += strArray[i].Length; if (totalLength < 0) { throw new OutOfMemoryException(); } } return ConcatArray(strArray, totalLength); } private static string ConcatArray(string[] values, int totalLength) { string dest = FastAllocateString(totalLength); int destPos = 0; for (int i = 0; i < values.Length; i++) { FillStringChecked(dest, destPos, values[i]); destPos += values[i].Length; } return dest; } private static unsafe void FillStringChecked(string dest, int destPos, string src) { int length = src.Length; if (length > (dest.Length - destPos)) { throw new IndexOutOfRangeException(); } fixed (char* chRef = &dest.m_firstChar) { fixed (char* chRef2 = &src.m_firstChar) { wstrcpy(chRef + destPos, chRef2, length); } } }
看到没有,先计算目标字符串的长度,然后申请相应的空间,最后逐一复制。相当简练,基本上可以说没有多余操作,时间复杂度仅为o(n),而且常数项为1。总结:固定数量的字符串连接效率最高的是string.Concat,而连+被编译器优化为string.Concat,所以+的效率也是最高的。
注意:字符串的连+不要拆成多条语句,比如:
string sql = "update tableName set int1="; sql += int1.ToString(); sql += ...
这样子的代码,编译器如果没有优化为string.Concat,也就变成了性能杀手,因为第i个字符串需要复制n-i次,时间复杂度就成了O(n^2)。
那如果字符串数量不固定怎么办呢?所以就有了StringBuilder,一般情况下它使用2n的空间来保证O(n)整体时间复杂度,常数项接近于2。
public StringBuilder() : this(0x10) { } public StringBuilder(int capacity) : this(string.Empty, capacity) { } public StringBuilder(string value, int capacity) : this(value, 0, (value != null) ? value.Length : 0, capacity) { } public StringBuilder(string value, int startIndex, int length, int capacity) { this.m_currentThread = Thread.InternalGetCurrentThread(); if (capacity < 0) { throw new ArgumentOutOfRangeException("capacity", string.Format(CultureInfo.CurrentCulture, Environment.GetResourceString("ArgumentOutOfRange_MustBePositive"), new object[] { "capacity" })); } if (length < 0) { throw new ArgumentOutOfRangeException("length", string.Format(CultureInfo.CurrentCulture, Environment.GetResourceString("ArgumentOutOfRange_MustBeNonNegNum"), new object[] { "length" })); } if (startIndex < 0) { throw new ArgumentOutOfRangeException("startIndex", Environment.GetResourceString("ArgumentOutOfRange_StartIndex")); } if (value == null) { value = string.Empty; } if (startIndex > (value.Length - length)) { throw new ArgumentOutOfRangeException("length", Environment.GetResourceString("ArgumentOutOfRange_IndexLength")); } this.m_MaxCapacity = 0x7fffffff; if (capacity == 0) { capacity = 0x10; } while (capacity < length) { capacity *= 2; if (capacity < 0) { capacity = length; break; } } this.m_StringValue = string.GetStringForStringBuilder(value, startIndex, length, capacity); }
我们看到,默认情况下,它初始化的时候申请一个长度为16的字符串作为容器。
public StringBuilder Append(string value) { if (value != null) { string stringValue = this.m_StringValue; IntPtr currentThread = Thread.InternalGetCurrentThread(); if (this.m_currentThread != currentThread) { stringValue = string.GetStringForStringBuilder(stringValue, stringValue.Capacity); } int length = stringValue.Length; int requiredLength = length + value.Length; if (this.NeedsAllocation(stringValue, requiredLength)) { string newString = this.GetNewString(stringValue, requiredLength); newString.AppendInPlace(value, length); this.ReplaceString(currentThread, newString); } else { stringValue.AppendInPlace(value, length); this.ReplaceString(currentThread, stringValue); } } return this; } private string GetNewString(string currentString, int requiredLength) { int maxCapacity = this.m_MaxCapacity; if (requiredLength < 0) { throw new OutOfMemoryException(); } if (requiredLength > maxCapacity) { throw new ArgumentOutOfRangeException("requiredLength", Environment.GetResourceString("ArgumentOutOfRange_SmallCapacity")); } int capacity = currentString.Capacity * 2; if (capacity < requiredLength) { capacity = requiredLength; } if (capacity > maxCapacity) { capacity = maxCapacity; } if (capacity <= 0) { throw new ArgumentOutOfRangeException("newCapacity", Environment.GetResourceString("ArgumentOutOfRange_NegativeCapacity")); } return string.GetStringForStringBuilder(currentString, capacity); }
当空间不够时,它重新申请至少两倍的空间以满足需求。我们可以看到,这种方式最坏的执行时间为n+n/2+n/4+n/8+...,接近于2n。因为这个算法的实用与高效,.net类库里面有很多动态集合都采用这种牺牲空间换取时间的方式,一般来说效果还是不错的。
但是StringBuilder相对于+=来说不够简洁,所以当n较小时一般使用O(n^2)的+=,当n稍大一点一般使用StringBuilder。
再来看看string.Format。有人说他比+效率高,因为它的底层是StringBuilder。很明显这种说法已经不攻自破了,抛开string.Format底层需要解析{?}的代价,StringBuilder自己就PK不过+,难道你是想PK+=,只能说你是故意的。
另外还有一种方式,那就是List<string>,我个人比较常用,也是我推荐使用的常用方式,因为它可以转换为string[]后使用string.Concat或string.Join,很多时候都比StringBuilder更高效。List与StringBuilder采用的是同样的动态集合算法,时间复杂度也是O(n),与StringBuilder不同的是:List的n是字符串的数量,复制的是字符串的引用;StringBuilder的n是字符串的长度,复制的数据。不同的特性决定的它们各自的适应环境,当子串比较大时建议使用List<string>,因为复制引用比复制数据划算。而当子串比较小,比如平均长度小于8,特别是一个一个的字符,建议使用StringBuilder。