编写高质量代码改善C#程序的157个建议——建议1:正确操作字符串
最近拜读了陆敏技老师的《编写高质量代码改善C#程序的157个建议》,感觉不错,决定把笔记整理一遍。
建议1: 正确操作字符串
字符串应该是所有编程语言中使用最频繁的一种基础数据类型。如果使用不慎,我们就会为一次字符串的操作所带来的额外性能开销而付出代价。本条建议将从两个方面来探讨如何规避这类性能开销:
- 确保尽量少的装箱
- 避免分配额外的内存空间
先来介绍第一个方面,请看下面的两行代码:
String str1 = "str1"+ ;
String str2 = "str2"+ .ToString();
为了清楚这两行代码的执行情况,我们来比较两者生成的IL代码。
第一行代码对应的IL代码如下:
.maxstack
IL_0000: ldstr "str1"
IL_0005: ldc.i4.s
IL_0007: box [mscorlib]System.Int32
IL_000c: call string [mscorlib]System.String::Concat(object, object)
IL_0011: pop
IL_0012: ret
第二行代码对应的IL代码如下:
.maxstack
.locals init ([] int32 CS$$)
IL_0000: ldstr "str2"
IL_0005: ldc.i4.s
IL_0007: stloc.
IL_0008: ldloca.s CS$$
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 = "" + s1 + ""; //以上两行代码创建了3个字符串对象,并执行了一次string.Contact方法
} private static void NewMethod6()
{
string re6 = + ""; //该代码发生一次装箱,并调用一次string.Contact方法
}
而在以下代码中,字符串不会在运行时拼接字符串,而是会在编译时直接生成一个字符串。
private static void NewMethod2()
{
string re2 = "" + "abc" + ""; //该代码等效于
//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指定的长度要合适,太小了,需要频繁分配内存;太大了,浪费空间。
曾经有人问我,下面的两种字符串拼接方式,哪种效率更高:
. private static void NewMethod8()
{
string a = "t";
a += "e";
a += "s";
a += "t";
} . 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);
}
转自:《编写高质量代码改善C#程序的157个建议》陆敏技
编写高质量代码改善C#程序的157个建议——建议1:正确操作字符串的更多相关文章
- 编写高质量代码改善C#程序的157个建议[1-3]
原文:编写高质量代码改善C#程序的157个建议[1-3] 前言 本文主要来学习记录前三个建议. 建议1.正确操作字符串 建议2.使用默认转型方法 建议3.区别对待强制转换与as和is 其中有很多需要理 ...
- 读书--编写高质量代码 改善C#程序的157个建议
最近读了陆敏技写的一本书<<编写高质量代码 改善C#程序的157个建议>>书写的很好.我还看了他的博客http://www.cnblogs.com/luminji . 前面部 ...
- 编写高质量代码改善C#程序的157个建议——建议157:从写第一个界面开始,就进行自动化测试
建议157:从写第一个界面开始,就进行自动化测试 如果说单元测试是白盒测试,那么自动化测试就是黑盒测试.黑盒测试要求捕捉界面上的控件句柄,并对其进行编码,以达到模拟人工操作的目的.具体的自动化测试请学 ...
- 编写高质量代码改善C#程序的157个建议——建议156:利用特性为应用程序提供多个版本
建议156:利用特性为应用程序提供多个版本 基于如下理由,需要为应用程序提供多个版本: 应用程序有体验版和完整功能版. 应用程序在迭代过程中需要屏蔽一些不成熟的功能. 假设我们的应用程序共有两类功能: ...
- 编写高质量代码改善C#程序的157个建议——建议155:随生产代码一起提交单元测试代码
建议155:随生产代码一起提交单元测试代码 首先提出一个问题:我们害怕修改代码吗?是否曾经无数次面对乱糟糟的代码,下决心进行重构,然后在一个月后的某个周一,却收到来自测试版的报告:新的版本,没有之前的 ...
- 编写高质量代码改善C#程序的157个建议——建议154:不要过度设计,在敏捷中体会重构的乐趣
建议154:不要过度设计,在敏捷中体会重构的乐趣 有时候,我们不得不随时更改软件的设计: 如果项目是针对某个大型机构的,不同级别的软件使用者,会提出不同的需求,或者随着关键岗位人员的更替,需求也会随个 ...
- 编写高质量代码改善C#程序的157个建议——建议153:若抛出异常,则必须要注释
建议153:若抛出异常,则必须要注释 有一种必须加注释的场景,即使异常.如果API抛出异常,则必须给出注释.调用者必须通过注释才能知道如何处理那些专有的异常.通常,即便良好的命名也不可能告诉我们方法会 ...
- 编写高质量代码改善C#程序的157个建议——建议152:最少,甚至是不要注释
建议152:最少,甚至是不要注释 以往,我们在代码中不写上几行注释,就会被认为是钟不负责任的态度.现在,这种观点正在改变.试想,如果我们所有的命名全部采用有意义的单词或词组,注释还有多少存在的价值. ...
- 编写高质量代码改善C#程序的157个建议——建议151:使用事件访问器替换公开的事件成员变量
建议151:使用事件访问器替换公开的事件成员变量 事件访问器包含两部分内容:添加访问器和删除访问器.如果涉及公开的事件字段,应该始终使用事件访问器.代码如下所示: class SampleClass ...
- 编写高质量代码改善C#程序的157个建议——建议150:使用匿名方法、Lambda表达式代替方法
建议150:使用匿名方法.Lambda表达式代替方法 方法体如果过小(如小于3行),专门为此定义一个方法就会显得过于繁琐.比如: static void SampeMethod() { List< ...
随机推荐
- INSTALL_FAILED_SHARED_USER_INCOMPATIBLE的问题
eclipse编译出来的apk,安装时报出INSTALL_FAILED_SHARED_USER_INCOMPATIBLE的错误. 原因:apk的AndroidManifest.xml中声明了andro ...
- LaunchImage添加以及设置无效处理
1.添加LaunchImage 2.添加所需要图片即可,出现un..可以删除,警告也随之而去,并删除LauchImage Assets之后重新添加 3.确定设置是否一样 4.发现启动后加载不了启动图, ...
- unittest--unittest.defaultTestLoader()的方法
unittest.defaultTestLoader(): defaultTestLoader()类,通过该类下面的discover()方法可自动更具测试目录start_dir匹配查找测试用例文件(t ...
- 分布式缓存系统 Memcached 状态机之SET、GET命令
首先对状态机中的各种状态做个简单总结,具体可见状态转换示意图: 1.listening:这个状态是主线程的默认状态,它只有这一个状态:负责监听socket,接收客户连接,将连接socket派发给工作线 ...
- 生成器+列表生成式,生成器可以节省内存,随时调取函数运行,以及实现多线程运行函数,__next__()和.send(参数)的区别,a,b=b,a+b其实是元祖的用法,出现异常状态用try...except StopIteration来处理
列表生成式:是代码更简洁. 也可以是函数,比如func(i) 生成器:generator 列表生成式,是中括号,改成小括号,就是生成器: 如果你用列表生成式,生成一亿个数据:这里会卡好久,会生成一亿个 ...
- Rest之路 - Rest架构中的重要概念
资源 在Rest的架构之内,讲一切内容都是为资源.每一个资源都被定义为一个URI. 格式: <protocol>://<service-name>/<ResourceTy ...
- linux lcd设备驱动剖析二
上一节中,分析了s3c2410fb,c的入口出口函数,以及一些重要结构体的分析,初步知道了这是一个平台驱动的架构. 上一节文章链接:http://blog.csdn.net/lwj103862095/ ...
- Python Twisted系列教程10:增强defer功能的客户端
作者:dave@http://krondo.com/an-introduction-to-asynchronous-programming-and-twisted/ 译者:杨晓伟(采用意译) 可以从这 ...
- 1.1.Task Queue
任务队列是一种跨线程.跨机器工作的一种机制. 任务队列中包含称作任务的工作单元.有专门的工作进程持续不断的监视任务队列,并从中获得新的任务并处理. celery通过消息进行通信,通常使用一 ...
- 用sysbench压测MySQL,通过orzdba监控MySQL
1.1 安装sysbench wget https://codeload.github.com/akopytov/sysbench/zip/0.5 unzip 0.5 cd sysbench-0.5/ ...