字符串连接,常用的三种方式: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是怎么实现的
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。
StringBuilder
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的字符串作为容器。
StringBuilder.Append
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