[读书笔记]C#学习笔记八:StringBuilder与String详解及参数传递问题剖析

 

前言

上次在公司开会时有同事分享windebug的知识, 拿的是string字符串Concat拼接 然后用while(true){}死循环的Demo来讲解.
其中有提及string操作大量字符串效率低下的问题, 刚好自己之前也看过类似的问题, 于是便拿出来记录一下.
本文内容: 参数传递问题剖析, string与stringbuilder详解

1,参数传递问题剖析

对于C#中的参数传递,根据参数的类型可以分为四类:

  • 值类型参数的按值传递
  • 引用类型参数的按值传递
  • 值类型参数的按引用传递
  • 引用类型参数的按引用传递

1.1值类型参数的按值传递

复制代码
 1 class Program
 2 {
 3     static void Main(string[] args)
 4     { 
 5         
 6         int addNum = 1;
 7         // addNum 就是实参,
 8         Add(addNum); 
 9      }
10 
11     // addnum就是形参,也就是被调用方法中的参数
12     private static void Add(int addnum)
13     {
14         addnum = addnum + 1;
15         Console.WriteLine(addnum);
16     }
17 }
复制代码

对于值类型的按值传递,传递的是该值类型实例的一个拷贝,也就是形参此时接受到的是实参的一个副本,被调用方法操作是实参的一个拷贝,所以此时并不影响原来调用方法中的参数值,为了证明这点,看看下面的代码和运行结果就明白了:

复制代码
 1 class Program
 2 {
 3     static void Main(string[] args)
 4     { 
 5         // 1. 值类型按值传递情况
 6         Console.WriteLine("按值传递的情况");
 7         int addNum = 1;
 8         Add(addNum);
 9         Console.WriteLine(addNum);   
10       
11         Console.Read();
12     }
13 
14     // 1. 值类型按值传递情况
15     private static void Add(int addnum)
16     {
17         addnum = addnum + 1;
18         Console.WriteLine(addnum);
19     }
20 }
复制代码

 

运行结果是: 
按值传递的情况
2
1
从结果中可以看出addNum调用方法之后它的值并没有改变,Add 方法的调用只是改变了addNum的副本addnum的值,所以addnum的值修改为2了。具体的分析请看下面的图:


1.2引用类型参数的按值传递
当传递的参数是引用类型的时候,传递和操作的是指向对象的引用(看到这里,有些朋友会觉得此时不是传递引用吗?怎么还是按值传递了?对于这个疑惑,此时确实是按值传递,此时传递的对象的地址,传递地址本身也是传递这个地址的值,所以此时仍然是按值传递的),此时方法的操作就会改变原来的对象。代码如下:

复制代码
 1 class Program
 2 {
 3     static void Main(string[] args)
 4     { 
 5         // 2. 引用类型按值传递情况
 6         RefClass refClass = new RefClass();
 7         AddRef(refClass);
 8         Console.WriteLine(refClass.addnum);
 9     }    
10      // 2. 引用类型按值传递情况
11     private static void AddRef(RefClass addnumRef)
12     {
13         addnumRef.addnum += 1;
14         Console.WriteLine(addnumRef.addnum);
15     }
16 }
17 class RefClass
18 {
19     public int addnum=1;
20 }
复制代码

运行结果为:
2
2
为什么此时传递引用就会修改原来实参中的值呢?对于这点我们还是参数在内存中分布图来解释下:

1.3string引用类型参数的按值传递的特殊情况
对于String类型同样是引用类型,然而对于string类型的按值传递时,此时引用类型的按值传递却不会修改实参的值,可能很多朋友对于这点很困惑,下面具体看看下面的代码:

复制代码
 1 class Program
 2 {
 3     static void Main(string[] args)
 4     { 
 5           // 3. String引用类型的按值传递的特殊情况
 6         string str = "old string";
 7         ChangeStr(str);
 8         Console.WriteLine(str);
 9         
10     }
11     
12      // 3. String引用类型的按值传递的特殊情况
13     private static void ChangeStr(string oldStr)
14     {
15         oldStr = "New string";
16         Console.WriteLine(oldStr);
17     }
18 }
复制代码

运行结果为:

New string
old string
对于为什么原来的值没有被改变主要是因为string的“不变性”,所以在被调用方法中执行 oldStr="New string"代码时,此时并不会直接修改oldStr中的"old string"值为"New string",因为string类型是不变的,不可修改的,此时内存会重新分配一块内存,然后把这块内存中的值修改为 “New string”,然后把内存中地址赋值给oldStr变量,所以此时str仍然指向 "old string"字符,而oldStr却改变了指向,它最后指向了 "New string"字符串。所以运行结果才会像上面这样,下面内存分布图可以帮助你更形象地理解文字表述:

1.4按引用传递

不管是值类型还是引用类型,我们都可以使用ref 或out关键字来实现参数的按引用传递,然而按引用进行传递的时候,需要注意下面两点:

方法的定义和方法调用都必须同时显式使用ref或out,否则会出现编译错误

CLR允许通过out 或ref参数来实现方法重载。如:

复制代码
 1 #region CLR 允许out或ref参数来实现方法重载
 2 private static void Add(string str)
 3 {
 4     Console.WriteLine(str);
 5 }
 6 
 7 // 编译器会认为下面的方法是另一个方法,从而实现方法重载
 8 private static void Add(ref string str)
 9 {
10     Console.WriteLine(str);
11 }
12 #endregion
复制代码

按引用传递可以解决由于值传递时改变引用副本而不影响引用本身的问题,此时传递的是引用的引用(也就是地址的地址),而不是引用的拷贝(副本)。下面就具体看看按引用传递的代码:

复制代码
 1 class Program
 2 {
 3     static void Main(string[] args)
 4     { 
 5         #region 按引用传递
 6         Console.WriteLine("按引用传递的情况");
 7         int num = 1;
 8         string refStr = "Old string";
 9         ChangeByValue(ref num);
10         Console.WriteLine(num);
11         changeByRef(ref refStr);
12         Console.WriteLine(refStr);
13         #endregion 
14 
15         Console.Read();
16     }
17 
18     #region 按引用传递
19     // 1. 值类型的按引用传递情况
20     private static void ChangeByValue(ref int numValue)
21     {
22         numValue = 10;
23         Console.WriteLine(numValue);
24     }
25 
26     // 2. 引用类型的按引用传递情况
27     private static void changeByRef(ref string numRef)
28     {
29         numRef = "new string";
30         Console.WriteLine(numRef);
31     }
32 
33     #endregion
34 }
复制代码

运行结果为:
按引用传递的情况
10
10
new string
new string
从运行结果可以看出,此时引用本身的值也被改变了,通过下面一张图来帮忙大家理解下按引用传递的方式:



到这里参数的传递所有内容就介绍完了。总之,对于按值传递,不管是值类型还是引用类型的按值传递,都是传递实参的一个拷贝,只是值类型时,此时传递的是实参实例的一个拷贝(也就是值类型值的一个拷贝),而引用类型时,此时传递的实参引用的副本。对于按引用传递,传递的都是参数地址,也就是实例的指针。

2, string与stringBuilder的内部实现
大家应该知道如果做大量的字符串拼接的话, string的效率明显是低于stringBuilder的, 至于示例我这里就不在列出了,下面给出个链接可以查看下.
我这里只是从string和stringBuilder源码说起, 通过源代码的实现方式来说明stringBuilder为何比string效率高.

StringBuilder vs String+String(String concatenation):
通常情况下,4~8个字符串之间的连接,String+String的效率更高。
答案来自: http://stackoverflow.com/a/1612819
StringBuilder vs String.concat(): 
如果在编译期间不能确定要连接的字符串个数,用StringBuilder更合适。
答案来自: http://stackoverflow.com/a/4191142

下面先给出结论:

stringbuilder内部维护一个字符数组。下次追加的字符串,直接占用空余的位置。 
如果超出上限。数组增大为原来的两倍(两倍还不够就直接增大到足够宽度),然后覆盖原来的数组.

String是不可改变的。每次使用System.String类中的方法之一时,都要在内存中创建一个新的字符串对象,这就需要为该新对象分配新的空间。
在需要对字符串执行重复修改的情况下,与创建新的String对象相关的系统开销可能会非常昂贵。

那么下面就看看string和stringBuilder源码有和区别吧, 我这里是使用的Reflector查看的:
(1)string

打开Reflector,找到string类

找到Concat方法, 我们这里以Concat为例:

下面我们在看下FillStringChecked(dest, 0, str0)的实现方式:


所以看到这里结论就出来了: 当我们队字符串进行大量操作的时候, 会产生很多的新的字符串, 这些字符串会大量零碎的占据着堆空间, 大多都是生存期较短的, 会对gc产生比较大的回收压力.


(2)stringBuilder

看这个类的话,还是看一下它的源代码,以Append吧,从下面这个截图中看出来几个有意思的地方。

<1> 原来StringBuilder里面维护的是一个m_ChunkChars的字符数组。

<2> 如果当前的字符串的length<2,会直接给chunkchars数组复制,length>2的时候看到的是刚才string类中经典的wstrcpy用法,而

      这个时候ptr指向的是chunkChars[chunkLength]的首地址,而不像string中申请新的内存空间,所以从这里看,比string大大的节省

    了内存空间。




更多细节内容请看@老赵点滴 大神的博客内容吧:

重谈字符串连接性能上:http://blog.zhaojie.me/2009/11/string-concat-perf-1-benchmark.html
重谈字符串连接性能中:http://blog.zhaojie.me/2009/12/string-concat-perf-2-stringbuilder-implementations.html
重谈字符串连接性能下:http://blog.zhaojie.me/2009/12/string-concat-perf-3-profiling-analysis.html