C#中如何正确的使用字符串String
前言
c#中提供了比较全面的字符串处理方法,很多函数都进行了封装为我们的编程工作提供了很大的便利。system.string是最常用的字符串操作类,可以帮助开发者完成绝大部分的字符串操作功能,使用方便。
字符串作为所有编程语言中使用最频繁的一种基础数据类型。如果使用不慎,将会造成不必要的内存开销,为此而付出代价。
而要优化此类型,从以下两点入手:
1、尽量少的装箱
2、避免分配额外的内存空间
先从第一点装箱的操作说起,查看如下代码:
//发生装箱的代码 string boxoperate = "test" + 4.5f;
其中间语言il代码为如下:
il_0000: nop il_0001: ldstr "test" il_0006: ldc.r4 4.5 il_000b: box [mscorlib]system.single il_0010: call string [mscorlib]system.string::concat(object, object) il_0015: stloc.0 il_0016: call valuetype [mscorlib]system.consolekeyinfo [mscorlib]system.console::readkey() il_001b: pop il_001c: ret
不难看出,上述代码发生了装箱的操作(il代码中的box).装箱之所以会发生性能损耗,因为它要完成如下三个步骤:
1、首先,会为值类型在托管堆中分配内存。除了值类型本身所分配的内存外,内存总量还要加上类型对象指针和同步块索引所占用的内存,
2、将值类型的值复制到新分配的堆内存中。
3、返回已经成为引用类型的对象的地址。
在来看以下代码:
//没有发生装箱的代码 string boxoperate = "test" + 4.tostring();
其中间il代码如下:
il_0000: nop il_0001: ldstr "test" il_0006: ldc.r4 4 il_000b: stloc.1 il_000c: ldloca.s 1 il_000e: call instance string [mscorlib]system.single::tostring() il_0013: call string [mscorlib]system.string::concat(string, string) il_0018: stloc.0 il_0019: call valuetype [mscorlib]system.consolekeyinfo [mscorlib]system.console::readkey() il_001e: pop il_001f: ret
如上,并没有发生任何装箱操作,但是达到的结果却是我们想要的。原因是 4.tostring() 这行代码并没有发生装箱行为,是实际调用的是整数型的tostring()方法,其原型如下:
public override string tostring(){ return number.formatint32(m_value, null, numberformat.currentinfo); }
可能有人会问,是不是原型中的 number.format_xxx方法会发生装箱行为呢?实际上,number.format_xxx方法是一个非托管的方法,其原型如下:
[methodimpl(methodimploptions.internalcall), securitycritical]
public statuc extern string formatint32(int value, string format,numberformatinfo info);
它是通过直接操作内存来完成 int32 到 string 的转换,效率要比装箱高得多。所以,在使用其他值引用类型到字符串得转换比完成拼接时,应当避免使用操作符 “+” 来我完成,而应该使用值引用类型提供得tostring方法。
也许有人会问:即使fcl提供得方法没有发生装箱行为,但在其他情况下,fcl方法内部会不会含有装箱的行为?也许会存在,所以,本人推荐:编写代码中,应当尽量避免发生不必要的装箱代码。
第二个方面:避免分配额外的空间。对于clr来说,string对象(字符串对象)是个很特殊的对象,它一旦被赋值就不可改变(在内存中)。在运行时调用system.string类中的任何方法或进行任何运算('=‘赋值,'+‘拼接等),都会在内存中创建一个新的字符串对象,这也意味着要为该新对象分配新的内存空间。如以下代码会带来额外开销。
private static void test(){ string str1 = "aa"; str1 = str1 + "123" + "345"; //以上代码创建了3个string对象,并执行了一次string.contact方法。 }
而在以下代码中,字符串不会在运行时拼接字符串,而是会在编译时直接生成一个字符串。
private static void test() { string str= "aa" + "123" + "345";//等效 string str= "aa123345"; } private static void test2() { const string str = "aa"; string newstr = "123" + str; //因为str是一个常量,所以该代码等效于 string newstr = "123" + “aa”; //最终等效于 string newstr = "123aa”; }
由于使用system.string类会在某些场合带来明显的性能损耗,所以微软另外提供了一个类型stringbuilder来弥补string的不足。
stringbuilder并不会重新创建一个string对象,它的效率源于预先以非托管的方式分配内存。如果stringbuilder没有先定义长度,则默认分配的长度为16。当stringbuilder的长度大于16小于32时,stringbuild又会重新分配内存,使之成为16的倍数。stringbuilder重新分配内存时按照上次的容量加倍进行分配的。注意:stringbuilder指定的长度要合适,太小了,需要频繁分配内存;太大了,浪费内存空间。
以下是例子举例:
private static string test3() { string a = "t"; a += "e"; a += "s"; a += "t"; return a; } private static string test4() { string a = "t"; string b = "e"; string c = "s"; string d = "t"; return a + b + c + d; } //以上两种效率都不高效。不要以为前者比后者创建的字符串对象更少,事实上,两者创建的字符串对象相等 //且前者进行了3次的string.contact方法调用,比后者还多了两次。
要完成上图的运行时的字符串拼接(注意:是运行时),更佳的做法是使用stringbuilder类型,代码如下:
private static string test5() { 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); return sb.tostring(); //因为说的是运行时,所以没必要使用以下代码 //stringbuilder sb = new stringbuilder("t"); //sb.append("e"); //sb.append("s"); //sb.append("t"); //return sb.tostring(); }
微软还提供了另外一个来简化这种操作,即使用string.format 方法。string.format方法在内部使用stringbuilder 进行字符串格式化,如下图代码:
private static string test6() { //为演示,定义4个变量 string a = "t"; string b = "e"; string c = "s"; string d = "t"; return string.format("{0}{1}{2}{3}", a, b, c, d); }
总结:
在使用string字符串时,应该尽量避免装箱操作和“+”连接操作。
好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对的支持。