购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

建议1:正确操作字符串

字符串应该是所有编程语言中使用最频繁的一种基础数据类型。如果使用不慎,我们就会为一次字符串的操作所带来的额外性能开销而付出代价。本条建议将从两个方面来探讨如何规避这类性能开销:

❑确保尽量少的装箱

❑避免分配额外的内存空间

先来介绍第一个方面,请看下面的两行代码:


1.String str1="str1"+9;

2.String str2="str2"+9.ToString();


为了清楚这两行代码的执行情况,我们来比较两者生成的IL代码。第一行代码对应的IL代码如下:


.maxstack 8

IL_0000:ldstr"str1"

IL_0005:ldc.i4.s 9

IL_0007:box[mscorlib]System.Int32

IL_000c:call string[mscorlib]System.String::Concat(object,object)

IL_0011:pop

IL_0012:ret


第二行代码对应的IL代码如下:


.maxstack 2

.locals init([0]int32 CS$0$0000)

IL_0000:ldstr"str2"

IL_0005:ldc.i4.s 9

IL_0007:stloc.0

IL_0008:ldloca.s CS$0$0000

IL_000a:call instance string[mscorlib]System.Int32::ToString()

IL_000f:call string[mscorlib]System.String::Concat(string,string)

IL_0014:pop

IL_0015:ret


可以看出,第一行代码"str1"+9在运行时会完成一次装箱行为(IL代码中的box);而第二行代码中的9.ToString()并没有发生装箱行为,它实际调用的是整型的ToString方法。ToString方法的原型为:


public override String ToString()

{

return Number.FormatInt32(m_value,null,NumberFormatInfo.CurrentInfo);

}


可能有人会问,是不是原型中的Number.FormatInt32方法会发生装箱行为呢?实际上,Number.FormatInt32方法是一个非托管的方法,其原型如下:


[MethodImpl(MethodImplOptions.InternalCall),SecurityCritical]

public static extern string FormatInt32(int value,string format,

NumberFormatInfo info);


它是通过直接操作内存来完成从int到string的转换,效率要比装箱高很多。所以,在使用其他值引用类型到字符串的转换并完成拼接时,应当避免使用操作符“+”来完成,而应该使用值引用类型提供的ToString方法。

也许有人还会提出疑问:上文所举的示例中,即使FCL提供的方法没有发生装箱行为,但在其他情况下,FCL方法内部会不会含有装箱的行为呢?答案是:也许会存在。不过,我们这里有一个指导原则:

在自己编写的代码中,应当尽可能地避免编写不必要的装箱代码。

注意 装箱之所以会带来性能损耗,因为它需要完成下面三个步骤:

1)首先,会为值类型在托管堆中分配内存。除了值类型本身所分配的内存外,内存总量还要加上类型对象指针和同步块索引所占用的内存。

2)将值类型的值复制到新分配的堆内存中。

3)返回已经成为引用类型的对象的地址。

第二个方面:避免分配额外的内存空间。对CLR来说,string对象(字符串对象)是个很特殊的对象,它一旦被赋值就不可改变。在运行时调用System.String类中的任何方法或进行任何运算(如“=”赋值、“+”拼接等),都会在内存中创建一个新的字符串对象,这也意味着要为该新对象分配新的内存空间。像下面的代码就会带来运行时的额外开销。


private static void NewMethod1()

{

string s1="abc";

s1="123"+s1+"456";//以上两行代码创建了3个

//字符串对象,并执行了一次string.Contact方法

}

private static void NewMethod6()

{

string re6=9+"456";//该代码发生一次装箱,并调

//用一次string.Contact方法

}


而在以下代码中,字符串不会在运行时拼接字符串,而是会在编译时直接生成一个字符串。


private static void NewMethod2()

{

string re2="123"+"abc"+"456";//该代码等效于

//string re2="123abc456";

}

private static void NewMethod9()

{

const string a="t";

string re1="abc"+a;//因为a是一个常量,所以

//该行代码等效于string re1="abc"+"t";

//最终等效于string re1="abct";

}


由于使用System.String类会在某些场合带来明显的性能损耗,所以微软另外提供了一个类型StringBuilder来弥补String的不足。

StringBuilder并不会重新创建一个string对象,它的效率源于预先以非托管的方式分配内存。如果StringBuilder没有先定义长度,则默认分配的长度为16。当StringBuilder字符长度小于等于16时,StringBuilder不会重新分配内存;当StringBuilder字符长度大于16小于32时,StringBuilder又会重新分配内存,使之成为16的倍数。在上面的代码中,如果预先判断字符串的长度将大于16,则可以为其设定一个更加合适的长度(如32)。StringBuilder重新分配内存时是按照上次的容量加倍进行分配的。当然,我们需要注意,StringBuilder指定的长度要合适,太小了,需要频繁分配内存;太大了,浪费空间。

曾经有人问我,下面的两种字符串拼接方式,哪种效率更高:


1.private static void NewMethod8()

{

string a="t";

a+="e";

a+="s";

a+="t";

}

2.private static void NewMethod7()

{

string a="t";

string b="e";

string c="s";

string d="t";

string result=a+b+c+d;

}


答案是:两者效率都不高。不要以为前者比后者创建的字符串对象更少,事实上,两者创建的字符串对象相等,且前者进行了3次string.Contact方法调用,比后者还多了两次。

要完成这样的运行时字符串拼接(注意:是运行时),更佳的做法是使用StringBuilder类型,代码如下所示:


private static void NewMethod10()

{

//为了演示的需要,定义了4个变量

string a="t";

string b="e";

string c="s";

string d="t";

StringBuilder sb=new StringBuilder(a);

sb.Append(b);

sb.Append(c);

sb.Append(d);

//再次提示,是运行时,所以没有使用下面的代码

//StringBuilder sb=new StringBuilder("t");

//sb.Append("e");

//sb.Append("s");

//sb.Append("t");

string result=sb.ToString();

}


微软还提供了另外一个方法来简化这种操作,即使用string.Format方法。string.Format方法在内部使用StringBuilder进行字符串的格式化,如下面的代码所示:


private static void NewMethod11()

{

//为了演示的需要,定义了4个变量

string a="t";

string b="e";

string c="s";

string d="t";

string.Format("{0}{1}{2}{3}",a,b,c,d);

} t+poLcEdKJskFZ58Zf9HqFJ7CaiEQ2ThG82XKxgzoUTLBuK9CmrzVyO3PvzC42hY


点击中间区域
呼出菜单
上一章
目录
下一章
×

打开