C#中如何正确的操作字符串?_.NET_编程开发_程序员俱乐部

中国优秀的程序员网站程序员频道CXYCLUB技术地图
热搜:
更多>>
 
您所在的位置: 程序员俱乐部 > 编程开发 > .NET > C#中如何正确的操作字符串?

C#中如何正确的操作字符串?

 2015/4/29 17:55:20  ludbul  程序员俱乐部  我要评论(0)
  • 摘要:字符串应该是所有编程语言中使用最频繁的一种基础数据类型。如果使用不慎,我们就会为一次字符串的操作所带来的额外性能开销而付出代价。本条建议将从两个方面来探讨如何规避这类性能开销:1.确保尽量少的装箱2.避免分配额外的内存空间。第一个方面:确保尽量少的装箱对于装拆箱,我们应该不陌生,值类型转换成引用类型即为装箱,引用类型转换成值类型即为拆箱。在自己编写的代码中,应当尽可能的避免编写不必要的装箱代码。装箱之所以会带来性能损耗,因为它需要完成下面三个步骤:•首先,会为值类型在托管堆中分配内存
  • 标签:C# 正确 操作 字符串

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

第一个方面:确保尽量少的装箱

对于装拆箱,我们应该不陌生,值类型转换成引用类型即为装箱, 引用类型转换成值类型即为拆箱。 在自己编写的代码中,应当尽可能的避免编写不必要的装箱代码。装箱之所以会带来性能损耗,因为它需要完成下面三个步骤:
• 首先,会为值类型在托管堆中分配内存。除了值类型本身所分配的内存外,内存总量还要加上类型对象指针和同步块索引所占用的内存。
• 然后,将值类型的值复制到新分配的堆内存中。
• 最后,返回已经成为引用类型的对象的地址。

下面是一行最简单的装箱代码

1 object obj = 1;

这行语句将整型常量1赋给object类型的变量obj; 众所周知常量1是值类型,值类型是要放在栈上的,而object是引用类型,它需要放在堆上;要把值类型放在堆上就需要执行一次装箱操作。

这行语句的IL代码如下,请注意注释部分说明:

.locals init (
[0] object objValue
) //以上三行IL表示声明object类型的名称为objValue的局部变量 
IL_0000: nop
IL_0001: ldc.i4.s 9 //表示将整型数9放到栈顶
IL_0003: box [mscorlib]System.Int32 //执行IL box指令,在内存堆中申请System.Int32类型需要的堆空间
IL_0008: stloc.0 //弹出堆栈上的变量,将它存储到索引为0的局部变量中

以上就是装箱所要执行的操作了,执行装箱操作时不可避免的要在堆上申请内存空间,并将堆栈上的值类型数据复制到申请的堆内存空间上,这肯定是要消耗内存和cpu资源的。我们再看下拆箱操作是怎么回事:

请看下面的C#代码:

object objValue = 4;
int value = (int)objValue;

上面的两行代码会执行一次装箱操作将整形数字常量4装箱成引用类型object变量objValue;然后又执行一次拆箱操作,将存储到堆上的引用变量objValue存储到局部整形值类型变量value中。

同样我们需要看下IL代码:

.locals init (
[0] object objValue,
[1] int32 'value'
) //上面IL声明两个局部变量object类型的objValue和int32类型的value变量
IL_0000: nop
IL_0001: ldc.i4.4 //将整型数字4压入栈
IL_0002: box [mscorlib]System.Int32 //执行IL box指令,在内存堆中申请System.Int32类型需要的堆空间
IL_0007: stloc.0 //弹出堆栈上的变量,将它存储到索引为0的局部变量中
IL_0008: ldloc.0//将索引为0的局部变量(即objValue变量)压入栈
IL_0009: unbox.any [mscorlib]System.Int32 //执行IL 拆箱指令unbox.any 将引用类型object转换成System.Int32类型
IL_000e: stloc.1 //将栈上的数据存储到索引为1的局部变量即value

拆箱操作的执行过程和装箱操作过程正好相反,是将存储在堆上的引用类型值转换为值类型并给值类型变量。

装箱操作和拆箱操作是要额外耗费CPU和内存资源的。那如何避免装箱和拆箱操作呢?有以下方法:
1. 用泛型集合取代ArrayList。
2. 用C#自带的转换方法,将值类型转换为引用类型。

下面我们看下使用泛型和不使用泛型引发装箱拆箱的情况。
1. 使用非泛型集合时引发的装箱和拆箱操作

看下面的一段代码:

var array = new ArrayList();
array.Add(1);
array.Add(2);

foreach (int value in array)
{
Console.WriteLine(“value is {0}”,value);
}

代码声明了一个ArrayList对象,向ArrayList中添加两个数字1,2;然后使用foreach将ArrayList中的元素打印到控制台。

在这个过程中会发生两次装箱操作和两次拆箱操作,在向ArrayList中添加int类型元素时会发生装箱,在使用foreach枚举ArrayList中的int类型元素时会发生拆箱操作,将object类型转换成int类型,在执行到Console.WriteLine时,还会执行两次的装箱操作;这一段代码执行了6次的装箱和拆箱操作;如果ArrayList的元素个数很多,执行装箱拆箱的操作会更多。

你可以通过使用ILSpy之类的工具查看IL代码的box,unbox指令查看装箱和拆箱的过程

2. 使用泛型集合的情况

请看如下代码:

1 var list = new List<int>();
2 list.Add(1);
3 list.Add(2);
4 
5 foreach (int value in list)
6 {
7 Console.WriteLine("value is {0}", value);
8 }

 

代码和1中的代码的差别在于集合的类型使用了泛型的List,而非ArrayList;我们同样可以通过查看IL代码查看装箱拆箱的情况,上述代码只会在Console.WriteLine()方法时执行2次装箱操作,不需要拆箱操作。

可以看出泛型可以避免装箱拆箱带来的不必要的性能消耗;当然泛型的好处不止于此,泛型还可以增加程序的可读性,使程序更容易被复用等等。

但是我们注意到,在使用泛型集合的时候,Console.WriteLine()方法时仍然执行2次装箱操作。能否将这两次装箱操作也优化掉呢?这就使用到了第二个方法,用C#自带的转换方法,将值类型转换为引用类型。如下:

var list = new List<int>();
list.Add(1);
list.Add(2);

foreach (int value in list)
{
Console.WriteLine(string.Format("value is {0}", value.ToString()));
}

再查看IL代码时可以发现,装箱操作已经被彻底消除了。它实际调用的是整形的ToString方法。ToString方法的原型为:

public override string ToString()
{
return Number.FormatInt32(m_value, null, NumberFormatInfo.CurrentInfo);
}

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

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

private static void Test6()
{
string s1 = "abc";
s1 = "123" + s1 + "456"; // 以上两行代码创建了3个String对象,并执行了一次String.Contact方法
string s2 = 9 + "456"; // 该代码发生一次装箱,并调用一次String.Concact方法
}

private static void Test7()
{
string s1 = "123" + "abc" + "456"; // 该代码等效于string s1 = "123abc456"
}    

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

StringBuilder并不会重新创建一个String对象,它的效率源于预先以非托管的方式分配内存。如果StringBuilder没有先定义长度,则默认分配的长度为16,当StringBuilder字符长度小于等于16时,StringBuilder不会重新分配内存。当StringBuilder字符长度大于16时小于32时,StringBuilder又会重新分配内存,使之成为16的倍数。在上面的代码中,如果预先判断字符串的长度将大于16,则可以为其设定一个更加合适的长度。
微软还提供了另外一个方法来简化这种操作,即使用string.Format方法。string.Format方法在内部使用StringBuilder进行字符串的格式化。

private static void Test9()
{
string a = "t";
string b = "e";
string c = "s";
string d = "t";

StringBuilder sb = new StringBuilder();
sb.Append(a);
sb.Append(b);
sb.Append(c);
sb.Append(d);

Console.WriteLine(sb.ToString());
}

private static void Test10()
{
string a = "t";
string b = "e";
string c = "s";
string d = "t";

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

最后总结:如何正确操作字符串:

1. 确保尽量少的拆装箱操作:使用泛型,使用ToString()将值类型转换为引用类型
2. 避免分配额外的内存空间:不用+=, +操作符, 使用StringBuilder, String.Format()链接多个String

 

参考引用列表:?

http://www.cnblogs.com/yukaizhao/archive/2011/10/18/csharp_box_unbox_1.html
http://www.cnblogs.com/yukaizhao/archive/2011/10/19/csharp_box_unbox_2.html
?《编写高质量代码:改善C#程序的157个建议》

发表评论
用户名: 匿名