编写高质量C#代码学习笔记(1)


建议1: 正确操作字符串

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

确保尽量少的装箱

避免分配额外的内存空间

先来介绍第一个方面,请看下面的两行代码:

  
  
  1. 1.  String str1 = "str1"+ 9;  
  2. 2.  String str2 = "str2"+ 9.ToString(); 

为了清楚这两行代码的执行情况,我们来比较两者生成的IL代码。

第一行代码对应的IL代码如下:

  
  
  1. .maxstack  8  
  2. IL_0000:  ldstr      "str1"  
  3. IL_0005:  ldc.i4.s   9  
  4. IL_0007:  box        [mscorlib]System.Int32  
  5. IL_000c:  call       string [mscorlib]System.String::Concat(object, object)  
  6. IL_0011:  pop  
  7. IL_0012:  ret 

第二行代码对应的IL代码如下:

  
  
  1. .maxstack  2  
  2. .locals init ([0] int32 CS$0$0000)  
  3. IL_0000:  ldstr      "str2"  
  4. IL_0005:  ldc.i4.s   9  
  5. IL_0007:  stloc.0  
  6. IL_0008:  ldloca.s   CS$0$0000  
  7. IL_000a:  call       instance string [mscorlib]System.Int32::ToString()  
  8. IL_000f:  call       string [mscorlib]System.String::Concat(string, string)  
  9. IL_0014:  pop  
  10. IL_0015:  ret 

可以看出,第一行代码“str1”+ 9在运行时会完成一次装箱行为(IL代码中的box);而第二行代码中的9.ToString()并没有发生装箱行为,它实际调用的是整型的ToString方法。ToString方法的原型为:

  
  
  1. public override String ToString()  
  2. {  
  3.     return Number.FormatInt32(m_value, null, NumberFormatInfo.CurrentInfo);  

可能有人会问,是不是原型中的Number.FormatInt32方法会发生装箱行为呢?实际上,Number.FormatInt32方法是一个非托管的方法,其原型如下:

  
  
  1. [MethodImpl(MethodImplOptions.InternalCall), SecurityCritical]  
  2.  public static extern string FormatInt32(int value, string format,  
  3.     NumberFormatInfo info); 

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

也许有人还会提出疑问:上文所举的示例中,即使FCL提供的方法没有发生装箱行为,但在其他情况下,FCL方法内部会不会含有装箱的行为呢?答案是:也许会存在。不过,我们这里有一个指导原则:

在自己编写的代码中,应当尽可能地避免编写不必要的装箱代码。

注意 装箱之所以会带来性能损耗,因为它需要完成下面三个步骤:

1)首先,会为值类型在托管堆中分配内存。除了值类型本身所分配的内存外,内存总量还要加上类型对象指针和同步块索引所占用的内存。

2)将值类型的值复制到新分配的堆内存中。

3)返回已经成为引用类型的对象的地址。

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

  
  
  1. private static void NewMethod1()  
  2. {  
  3.     string s1 = "abc";  
  4.     s1 = "123" + s1 + "456";    //以上两行代码创建了3个  
  5.         //字符串对象,并执行了一次string.Contact方法  
  6. }  
  7.  
  8. private static void NewMethod6()  
  9. {  
  10.     string re6 = 9 + "456";     //该代码发生一次装箱,并调  
  11.         //用一次string.Contact方法  

而在以下代码中,字符串不会在运行时拼接字符串,而是会在编译时直接生成一个字符串。

  
  
  1. private static void NewMethod2()  
  2. {  
  3.     string re2 = "123" + "abc" + "456"; //该代码等效于  
  4.         //string re2 = "123abc456";  
  5. }  
  6.  
  7. private static void NewMethod9()  
  8. {  
  9.     const string a = "t";  
  10.     string re1 = "abc" + a;     //因为a是一个常量,所以  
  11.     //该行代码等效于 string re1 = "abc" + "t";  
  12.     //最终等效于string re1 = "abct";  

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

StringBuilder并不会重新创建一个string 对象,它的效率源于预先以非托管的方式分配内存。如果StringBuilder 没有先定义长度,则默认分配的长度为16。当 StringBuilder 字符长度小于等于 16时,StringBuilder 不会重新分配内存;当 StringBuilder 字符长度大于16 小于 32时,StringBuilder 又会重新分配内存,使之成为 16的倍数。在上面的代码中,如果预先判断字符串的长度将大于16,则可以为其设定一个更加合适的长度(如32)。StringBuilder重新分配内存时是按照上次的容量加倍进行分配的。当然,我们需要注意,StringBuilder指定的长度要合适,太小了,需要频繁分配内存;太大了,浪费空间。

曾经有人问我,下面的两种字符串拼接方式,哪种效率更高:

  
  
  1. 1.      private static void NewMethod8()  
  2.         {  
  3.             string a = "t";  
  4.             a += "e";  
  5.             a += "s";  
  6.             a += "t";  
  7.         }  
  8.  
  9. 2.      private static void NewMethod7()  
  10.         {  
  11.             string a = "t";  
  12.             string b = "e";  
  13.             string c = "s";  
  14.             string d = "t";  
  15.             string result = a + b + c + d;  
  16.         } 

答案是:两者效率都不高。不要以为前者比后者创建的字符串对象更少,事实上,两者创建的字符串对象相等,且前者进行了3次string.Contact方法调用,比后者还多了两次。

要完成这样的运行时字符串拼接(注意:是运行时),更佳的做法是使用StringBuilder类型,代码如下所示:

  
  
  1. private static void NewMethod10()  
  2. {  
  3.     //为了演示的需要,定义了4个变量  
  4.     string a = "t";  
  5.     string b = "e";  
  6.     string c = "s";  
  7.     string d = "t";  
  8.     StringBuilder sb = new StringBuilder(a);  
  9.     sb.Append(b);  
  10.     sb.Append(c);  
  11.     sb.Append(d);  
  12.     //再次提示,是运行时,所以没有使用下面的代码  
  13.     //StringBuilder sb = new StringBuilder("t");  
  14.     //sb.Append("e");  
  15.     //sb.Append("s");  
  16.     //sb.Append("t");  
  17.     string result = sb.ToString();  

微软还提供了另外一个方法来简化这种操作,即使用string.Format方法。string.Format方法在内部使用StringBuilder进行字符串的格式化,如下面的代码所示:

  
  
  1. private static void NewMethod11()  
  2. {  
  3.     //为了演示的需要,定义了4个变量  
  4.     string a = "t";  
  5.     string b = "e";  
  6.     string c = "s";  
  7.     string d = "t";  
  8.     string.Format("{0}{1}{2}{3}", a, b, c, d);  

建议4: TryParse比Parse好

如果注意观察除string外的所有基元类型,会发现它们都有两个将字符串转型为本身的方法:Parse和TryParse。以类型double为例,这两个方法最简单的原型为:

   
   
  1. 1.public static double Parse(string s)  
  2. 2.public static bool TryParse(string s, out double result) 

两者最大的区别是,如果字符串格式不满足转换的要求,Parse方法将会引发一个异常;TryParse方法则不会引发异常,它会返回false,同时将result置为0。

实际上,早期的FCL中并没有提供TryParse方法,那时只能调用Parse方法,如果转型失败,则要将值设定为一个初始值,同时必须要捕获异常,代码如下所示:

   
   
  1. string str = null;  
  2. double d;  
  3. try  
  4. {  
  5.     d = Double.Parse(str);  
  6. }  
  7. catch (Exception ex)  
  8. {  
  9.     d = 0;  

要注意,引发异常这个过程会对性能造成损耗(第5章会详细解释这一点)。微软的开发团队正是注意到这一点,所以从.NET 2.0开始,FCL中开始为基元类型提供TryParse方法。

建议5: 使用int?来确保值类型也可以为null 

  
  
  1. int? i = 123;  
  2. int j = i ?? 0; 

int j = i ?? 0;表示的意思是,如果i的HasValue为true,则将i的value赋值给j;否则,就给j赋值为0。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值