C#中如何正确的操作字符串?
字符串应该是所有编程语言中使用最频繁的一种基础数据类型。如果使用不慎,我们就会为一次字符串的操作所带来的额外性能开销而付出代价。本条建议将从两个方面来探讨如何规避这类性能开销:
1. 确保尽量少的装箱
2. 避免分配额外的内存空间。
第一个方面:确保尽量少的装箱
对于装拆箱,我们应该不陌生,值类型转换成引用类型即为装箱, 引用类型转换成值类型即为拆箱。 在自己编写的代码中,应当尽可能的避免编写不必要的装箱代码。装箱之所以会带来性能损耗,因为它需要完成下面三个步骤:
• 首先,会为值类型在托管堆中分配内存。除了值类型本身所分配的内存外,内存总量还要加上类型对象指针和同步块索引所占用的内存。
• 然后,将值类型的值复制到新分配的堆内存中。
• 最后,返回已经成为引用类型的对象的地址。
下面是一行最简单的装箱代码
object obj = ;
这行语句将整型常量1赋给object类型的变量obj; 众所周知常量1是值类型,值类型是要放在栈上的,而object是引用类型,它需要放在堆上;要把值类型放在堆上就需要执行一次装箱操作。
这行语句的IL代码如下,请注意注释部分说明:
.locals init (
[] object objValue
) //以上三行IL表示声明object类型的名称为objValue的局部变量
IL_0000: nop
IL_0001: ldc.i4.s //表示将整型数9放到栈顶
IL_0003: box [mscorlib]System.Int32 //执行IL box指令,在内存堆中申请System.Int32类型需要的堆空间
IL_0008: stloc. //弹出堆栈上的变量,将它存储到索引为0的局部变量中
以上就是装箱所要执行的操作了,执行装箱操作时不可避免的要在堆上申请内存空间,并将堆栈上的值类型数据复制到申请的堆内存空间上,这肯定是要消耗内存和cpu资源的。我们再看下拆箱操作是怎么回事:
请看下面的C#代码:
object objValue = ;
int value = (int)objValue;
上面的两行代码会执行一次装箱操作将整形数字常量4装箱成引用类型object变量objValue;然后又执行一次拆箱操作,将存储到堆上的引用变量objValue存储到局部整形值类型变量value中。
同样我们需要看下IL代码:
.locals init (
[] object objValue,
[] int32 'value'
) //上面IL声明两个局部变量object类型的objValue和int32类型的value变量
IL_0000: nop
IL_0001: ldc.i4. //将整型数字4压入栈
IL_0002: box [mscorlib]System.Int32 //执行IL box指令,在内存堆中申请System.Int32类型需要的堆空间
IL_0007: stloc. //弹出堆栈上的变量,将它存储到索引为0的局部变量中
IL_0008: ldloc.//将索引为0的局部变量(即objValue变量)压入栈
IL_0009: unbox.any [mscorlib]System.Int32 //执行IL 拆箱指令unbox.any 将引用类型object转换成System.Int32类型
IL_000e: stloc. //将栈上的数据存储到索引为1的局部变量即value
拆箱操作的执行过程和装箱操作过程正好相反,是将存储在堆上的引用类型值转换为值类型并给值类型变量。
装箱操作和拆箱操作是要额外耗费CPU和内存资源的。那如何避免装箱和拆箱操作呢?有以下方法:
1. 用泛型集合取代ArrayList。
2. 用C#自带的转换方法,将值类型转换为引用类型。
下面我们看下使用泛型和不使用泛型引发装箱拆箱的情况。
1. 使用非泛型集合时引发的装箱和拆箱操作
看下面的一段代码:
var array = new ArrayList();
array.Add();
array.Add(); foreach (int value in array)
{
Console.WriteLine(“value is {}”,value);
}
代码声明了一个ArrayList对象,向ArrayList中添加两个数字1,2;然后使用foreach将ArrayList中的元素打印到控制台。
在这个过程中会发生两次装箱操作和两次拆箱操作,在向ArrayList中添加int类型元素时会发生装箱,在使用foreach枚举ArrayList中的int类型元素时会发生拆箱操作,将object类型转换成int类型,在执行到Console.WriteLine时,还会执行两次的装箱操作;这一段代码执行了6次的装箱和拆箱操作;如果ArrayList的元素个数很多,执行装箱拆箱的操作会更多。
你可以通过使用ILSpy之类的工具查看IL代码的box,unbox指令查看装箱和拆箱的过程
2. 使用泛型集合的情况
请看如下代码:
var list = new List<int>();
list.Add();
list.Add(); foreach (int value in list)
{
Console.WriteLine("value is {0}", value);
}
代码和1中的代码的差别在于集合的类型使用了泛型的List,而非ArrayList;我们同样可以通过查看IL代码查看装箱拆箱的情况,上述代码只会在Console.WriteLine()方法时执行2次装箱操作,不需要拆箱操作。
可以看出泛型可以避免装箱拆箱带来的不必要的性能消耗;当然泛型的好处不止于此,泛型还可以增加程序的可读性,使程序更容易被复用等等。
但是我们注意到,在使用泛型集合的时候,Console.WriteLine()方法时仍然执行2次装箱操作。能否将这两次装箱操作也优化掉呢?这就使用到了第二个方法,用C#自带的转换方法,将值类型转换为引用类型。如下:
var list = new List<int>();
list.Add();
list.Add(); 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 = "" + s1 + ""; // 以上两行代码创建了3个String对象,并执行了一次String.Contact方法
string s2 = + ""; // 该代码发生一次装箱,并调用一次String.Concact方法
} private static void Test7()
{
string s1 = "" + "abc" + ""; // 该代码等效于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个建议》
C#中如何正确的操作字符串?的更多相关文章
- 巧用JS中的join方法操作字符串
1.将数组的元素组起一个字符串,以separator为分隔符,省略的话则用默认用逗号为分隔符 /** *把数组转换成特定符号分割的字符串 */ function arrayToString(arr,s ...
- Python:常见操作字符串的函数
Python中提供了很多操作字符串的函数: string = "hello, my dear python!" string.capitalize() #将字符串中的第一个字母大写 ...
- 编写高质量代码改善C#程序的157个建议[正确操作字符串、使用默认转型方法、却别对待强制转换与as和is]
前言 本文主要来学习记录前三个建议. 建议1.正确操作字符串 建议2.使用默认转型方法 建议3.区别对待强制转换与as和is 其中有很多需要理解的东西,有些地方可能理解的不太到位,还望指正. 建议1. ...
- 编写高质量代码改善C#程序的157个建议——建议1:正确操作字符串
最近拜读了陆敏技老师的<编写高质量代码改善C#程序的157个建议>,感觉不错,决定把笔记整理一遍. 建议1: 正确操作字符串 字符串应该是所有编程语言中使用最频繁的一种基础数据类型.如果使 ...
- C#程序编写高质量代码改善的157个建议[正确操作字符串、使用默认转型方法、却别对待强制转换与as和is]
前言 本文主要来学习记录前三个建议. 建议1.正确操作字符串 建议2.使用默认转型方法 建议3.区别对待强制转换与as和is 其中有很多需要理解的东西,有些地方可能理解的不太到位,还望指正. 建议1. ...
- jsp中常用操作字符串的el表达式
由于在JSP页面中显示数据时,经常需要对显示的字符串进行处理,SUN公司针对于一些常见处理定义了一套EL函数库供开发者使用. 准备工作:1)导入jar包:standard.jar和jstl.jar2) ...
- java 中操作字符串都有哪些类?(未完成)它们之间有什么区别?(未完成)
java 中操作字符串都有哪些类?(未完成)它们之间有什么区别?(未完成)
- Golang中如何正确的使用sarama包操作Kafka?
Golang中如何正确的使用sarama包操作Kafka? 一.背景 在一些业务系统中,模块之间通过引入Kafka解藕,拿IM举例(图来源): 用户A给B发送消息,msg_gateway收到消息后,投 ...
- Jsp中使用EL表达式对字符串进行操作
用fn函数:<%@ taglib prefix="fn" uri="http://Java.sun.com/jsp/jstl/functions" %&g ...
随机推荐
- HDU-4300 Clairewd’s message
http://acm.hdu.edu.cn/showproblem.php?pid=4300 很难懂题意.... Clairewd’s message Time Limit: 2000/1000 MS ...
- Oracle SQL编写注意事项
1.SQL语句用大写的;因为Oracle总是先解析SQL语句,把小写的字母转换成大写的再执行. 2.数据表最好起别名;因为便于sql优化器快速分析. 3.尽量不要使用 insert into tabl ...
- List<T>的IndexOf方法和Remove方法
Microsoft地址 List<T>的IndexOf()方法 如果T是值类型的,就按照比较值的方法从列表的第一个元素开始逐个匹配,如果T是引用类型,就比较引用是否相同 举例如下: cla ...
- java基础(十九)IO流(二)
这里有我之前上课总结的一些知识点以及代码大部分是老师讲的笔记 个人认为是非常好的,,也是比较经典的内容,真诚的希望这些对于那些想学习的人有所帮助! 由于代码是分模块的上传非常的不便.也比较多,讲的也是 ...
- FireMonkey隐藏任务栏图标
FMX(FireMonkey)可以轻松实现很多VCL无法或难以实现的特效,所以将FMX程序作为界面,打包入DLL由VCL程序调用,是一个不错的方案.为了程序的完整性,你不想看见FMX程序在任务栏上显示 ...
- 【转】shell 教程——06 Shell变量:Shell变量的定义、删除变量、只读变量、变量类型
Shell支持自定义变量. 定义变量 定义变量时,变量名不加美元符号($),如: variableName="value" 注意,变量名和等号之间不能有空格,这可能和你熟悉的所有编 ...
- /etc/init.d/syslog: No such file or directory
centos 6 使用的是 rsyslog 而 centos5 使用 syslog ,兩個不同版本使用軟體不同.因此,你得要修訂 /etc/rsyslog.conf 才行!軟體也變成 /etc/ini ...
- opencv学习笔记-图像对比度、亮度调节
在数学中我们学过线性理论,在图像亮度和对比度调节中同样适用,看下面这个公式: 在图像像素中其中: 参数f(x)表示源图像像素. 参数g(x) 表示输出图像像素. 参数a(需要满足a>0)被称为增 ...
- 推荐一个可视化的学习Git的好网站:LearnGitBranching
博客搬到了fresky.github.io - Dawei XU,请各位看官挪步.最新的一篇是:推荐一个可视化的学习Git的好网站:LearnGitBranching.
- iOS 开发中常见的设计模式
最近有小伙伴问到在iOS开发中的几种设计模式,这里摘录一下别人的总结(因为已经感觉总结得差不多了,适用的可以阅读一下) 首先是开发中的23中设计模式分为三大类:1.创建型 2.结构型 3.行为型 (i ...