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

最近拜读了陆敏技老师的《编写高质量代码改善C#程序的157个建议》,感觉不错,决定把笔记整理一遍。

建议1: 正确操作字符串

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

  • 确保尽量少的装箱
  • 避免分配额外的内存空间

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

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

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

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

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

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

    .maxstack  2
    .locals init ([0] int32 CS$0$0000)
    IL_0000:  ldstr      "str2"
    IL_0005:  ldc.i4.s   9
    IL_0007:  stloc.0
    IL_0008:  ldloca.s   CS$0$0000
    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 = "123" + s1 + "456";    //以上两行代码创建了3个字符串对象,并执行了一次string.Contact方法
    }  

    private static void NewMethod6()
    {
        string re6 = 9 + "456";     //该代码发生一次装箱,并调用一次string.Contact方法
    } 

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

    private static void NewMethod2()
    {
        string re2 = "123" + "abc" + "456"; //该代码等效于
        //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指定的长度要合适,太小了,需要频繁分配内存;太大了,浪费空间。

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

    1.      private static void NewMethod8()
            {
                string a = "t";
                a += "e";
                a += "s";
                a += "t";
            }  

    2.      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个建议》陆敏技

时间: 2024-10-07 05:31:32

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

编写高质量代码改善C#程序的157个建议——建议45:为泛型类型参数指定逆变

建议45:为泛型类型参数指定逆变 逆变是指方法的参数可以是委托或者泛型接口的参数类型的基类.FCL4.0中支持逆变的常用委托有: Func<int T,out TResult> Predicate<in T> 常用委托有: IComparer<in T> 下面例子演示了泛型类型参数指定逆变所带来的好处: class Program { static void Main() { Programmer p = new Programmer { Name = "Mi

编写高质量代码改善C#程序的157个建议——建议27:在查询中使用Lambda表达式

建议27:在查询中使用Lambda表达式 LINQ实际上是基于扩展方法和Lambda表达式的.任何LINQ查询都能通过扩展方法的方式来代替. var personWithCompanyList = from person in personList select new { PersonName = person.Name, CompanyName = person.CompanyID==0?"Micro":"Sun" }; foreach (var item in

编写高质量代码改善C#程序的157个建议——建议26:使用匿名类型存储LINQ查询结果

建议26:使用匿名类型存储LINQ查询结果 从.NET3.0开始,C#开始支持一个新特性:匿名类型.匿名类型有var.赋值运算符和一个非空初始值(或以new开头的初始化项)组成.匿名类型有如下基本特性: 即支持简单类型也指出复杂类型.简单类型必须是一个非空初始值,复杂类型则是一个以new开头的初始化项. 匿名类型的属性是只读的,没有属性设置器,它一旦被初始化就不可更改. 如果两个匿名类型的属性值相同,那么就认为这两个匿名类型相等. 匿名类型可以再循环中用作初始化器. 匿名类型支持智能感知. 匿名

编写高质量代码改善C#程序的157个建议——建议20:使用泛型集合代替非泛型集合

建议20:使用泛型集合代替非泛型集合 在建议1中我们知道,如果要让代码高效运行,应该尽量避免装箱和拆箱,以及尽量减少转型.很遗憾,在微软提供给我们的第一代集合类型中没有做到这一点,下面我们看ArrayList这个类的使用情况: ArrayList al=new ArrayList(); al.Add(0); al.Add(1); al.Add("mike"); foreach (var item in al) { Console.WriteLine(item); } 上面这段代码充分演

编写高质量代码改善C#程序的157个建议——建议12: 重写Equals时也要重写GetHashCode

建议12: 重写Equals时也要重写GetHashCode 除非考虑到自定义类型会被用作基于散列的集合的键值:否则,不建议重写Equals方法,因为这会带来一系列的问题. 如果编译上一个建议中的Person这个类型,编译器会提示这样一个信息: “重写 Object.Equals(object o)但不重写 Object.GetHashCode()” 如果重写Equals方法的时候不重写GetHashCode方法,在使用如FCL中的Dictionary类时,可能隐含一些潜在的Bug.还是针对上一

编写高质量代码改善C#程序的157个建议——建议13: 为类型输出格式化字符串

建议13: 为类型输出格式化字符串 有两种方法可以为类型提供格式化的字符串输出.一种是意识到类型会产生格式化字符串输出,于是让类型继承接口IFormattable.这对类型来 说,是一种主动实现的方式,要求开发者可以预见类型在格式化方面的要求.更多的时候,类型的使用者需为类型自定义格式化器,这就是第二种方法,也是最灵活 多变的方法,可以根据需求的变化为类型提供多个格式化器.下面就来详细介绍这两种方法. 最简单的字符串输出是为类型重写ToString方法,如果没有为类型重写该方法,默认会调用Obj

编写高质量代码改善C#程序的157个建议——建议90:不要为抽象类提供公开的构造方法

建议90:不要为抽象类提供公开的构造方法 首先,抽象类可以有构造方法.即使没有为抽象类指定构造方法,编译器也会为我们生成一个默认的protected的构造方法.下面是一个标准的最简单的抽象类: abstract class MyAbstractClass { protected MyAbstractClass(){} } 其次,抽象类的方法不应该是public或internal的.抽象类设计的本意是让子类继承,而不是用于生成实例对象的.如果抽象类是public或internal的,它对于其它类型

编写高质量代码改善C#程序的157个建议——建议85:Task中的异常处理

建议85:Task中的异常处理 在任何时候,异常处理都是非常重要的一个环节.多线程与并行编程中尤其是这样.如果不处理这些后台任务中的异常,应用程序将会莫名其妙的退出.处理那些不是主线程(如果是窗体程序,那就是UI主线程)产生的异常,最终的办法都是将其包装到主线程上. 在任务并行库中,如果对任务运行Wait.WaitAny.WaitAll等方法,或者求Result属性,都能捕获到AggregateException异常.可以将AggregateException异常看做是任务并行库编程中最上层的异

编写高质量代码改善C#程序的157个建议——建议89:在并行方法体中谨慎使用锁

建议89:在并行方法体中谨慎使用锁 除了建议88所提到的场合,要谨慎使用并行的情况还包括:某些本身就需要同步运行的场合,或者需要较长时间锁定共享资源的场合. 在对整型数据进行同步操作时,可以使用静态类Interlocked的Add方法,这就极大地避免了由于进行原子操作长时间锁定某个共享资源所带来的同步性能损耗.回顾建议83中的例子. static void Main(string[] args) { int[] nums = new int[] { 1, 2, 3, 4 }; int total

编写高质量代码改善C#程序的157个建议——建议87:区分WPF和WinForm的线程模型

建议87:区分WPF和WinForm的线程模型 WPF和WinForm窗体应用程序都有一个要求,那就是UI元素(如Button.TextBox等)必须由创建它的那个线程进行更新.WinForm在这方面的限制并不是很严格,所以像下面这样的代码,在WinForm中大部分情况下还能运行(本建议后面会详细解释为什么会出现这种现象): private void buttonStartAsync_Click(object sender, EventArgs e) { Task t = new Task(()