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的转换,效率要比装箱高很多。所以,在使用其他值类型到字符串的转换并完成拼接时,应当避免使用操作符“+”来完成,而应该使用值类型提供的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个建议》

时间: 2024-09-30 09:21:03

C#中如何正确的操作字符串?的相关文章

编写高质量代码改善C#程序的157个建议[正确操作字符串、使用默认转型方法、却别对待强制转换与as和is]

前言 本文主要来学习记录前三个建议. 建议1.正确操作字符串 建议2.使用默认转型方法 建议3.区别对待强制转换与as和is 其中有很多需要理解的东西,有些地方可能理解的不太到位,还望指正. 建议1.正确操作字符串 字符串应该是所有编程语言中使用最频繁的一种基础数据类型.如果使用不慎,我们就会为一次字符串的操作所带来的额外性能开销而付出代价.本条建议将从两个方面来探讨如何规避这类性能开销: 1.确保尽量少的装箱 2.避免分配额外的内存空间 先来介绍第一个方面,请看下面的两行代码: String

编写高质量代码改善C#程序的157个建议——建议1:正确操作字符串

最近拜读了陆敏技老师的<编写高质量代码改善C#程序的157个建议>,感觉不错,决定把笔记整理一遍. 建议1: 正确操作字符串 字符串应该是所有编程语言中使用最频繁的一种基础数据类型.如果使用不慎,我们就会为一次字符串的操作所带来的额外性能开销而付出代价.本条建议将从两个方面来探讨如何规避这类性能开销: 确保尽量少的装箱 避免分配额外的内存空间 先来介绍第一个方面,请看下面的两行代码: String str1 = "str1"+ 9; String str2 = "

jsp中常用操作字符串的el表达式

由于在JSP页面中显示数据时,经常需要对显示的字符串进行处理,SUN公司针对于一些常见处理定义了一套EL函数库供开发者使用. 准备工作:1)导入jar包:standard.jar和jstl.jar2)在页面中使用JSTL定义的EL函数:<%@  taglib  uri="http://java.sun.com/jsp/jstl/functions"   prefix="fn"% 示例:转小写: ${fn:toLowerCase("www.ITCAST

建议1:正确操作字符串

如何规避使用不慎,带来的额外的性能开销: 1.确保尽量少的装箱 在自己编写的代码中,应当尽可能地避免编写不必要的装箱代码. 装箱操作会带来性能损耗的原因: a.首先,会为值类型在托管堆中分配内存.除了值类型本身所分配的内存外,内存总量还要加上类型对象指针和同步块索引所占的内存. b.将值类型的值赋值到新分配的堆内存中. c.返回已经成为引用类型的对象的地址. 2.避免分配额外的内存空间 对CLR来说,string对象是个很特殊的对象,它一旦被赋值就不可改变.在运行时调用System.String

java中操作字符串都有哪些类?它们之间有什么区别?

Java 中,常用的对字符串操作的类有 String.StringBuffer.StringBuilder String : final 修饰,String 类的方法都是返回 new String.即对 String 对象的任何改变都不影响到原对象,对字符串的修改操作都会生成新的对象. StringBuffer : 对字符串的操作的方法都加了synchronized,保证线程安全. StringBuilder : 不保证线程安全,在方法体内需要进行字符串的修改操作,可以 new StringBu

Jsp中使用EL表达式对字符串进行操作

用fn函数:<%@ taglib prefix="fn" uri="http://Java.sun.com/jsp/jstl/functions" %> 下面是JSTL中自带的方法列表以及其描述 函数名 函数说明 使用举例 fn:contains 判定字符串是否包含另外一个字符串 <c:if test="${fn:contains(name, searchString)}"> fn:containsIgnoreCase 判

c语言中读入带空格的字符串

http://blog.csdn.net/pipisorry/article/details/37073023 问题: scanf("%s", a); 运行输入hello world 回车 则输入到a的只是空格之前的部分,怎样把空格之后的部分也输出? 1. scanf( "%[^\n]", str ); #include <stdio.h> int main(){ char str[50]; scanf( "%[^\n]", str

EF5.0中的跨数据库操作

以前在用MVC + EF 的项目中,都是一个数据库,一个DbContext,因此一直没有考虑过在MVC+EF的环境下对于多个数据库的操作问题.等到要使用时,才发现这个问题也不小(关键是有个坑).直接说这个问题的解决流程吧: 1)由以往的经验:在ADO.NET中,要操作多个数据库,那就多写两个SqlHelper吧,或封装下,能动态的修改链接字符串.因此想到在这里能不能建多个ADO.NET实体数据模型呢?于是乎就有了这样两个文件:SealDBModel.edmx及UcmsDBModel.edmx.

【转】Windows内核下操作字符串!

* Windows内核下操作字符串! */ #include <ntddk.h> #include <ntstrsafe.h> #define BUFFER_SIZE 1024 VOID DriverUnload(IN PDRIVER_OBJECT pDriverObject) { KdPrint(("DriverUnload Load...\n")); } //==================================================