[mqt_2003的专栏]String和StringBuilder(C#)

         第一篇: 你真的了解.NET中的String

注:来自于网络

       String在任何语言中,都有它的特殊性,在.NET中也是如此。它属于基本数据类型,也是基本数据类型中唯一的引用类型。字符串可以声明为常量,但是它却放在了堆中。希望通过本文能够使大家对.NET中的String有一个深入的了解。
      
一:不可改变对象

在.NET中String是不可改变对象,一旦创建了一个String对象并为它赋值,它就不可能再改变,也就是你不可能改变一个字符串的值。这句话初听起来似乎有些不可思议,大家也许马上会想到字符串的连接操作,我们不也可以改变字符串吗?看下面这段代码:

 1 using System;
 2
 3 namespace Demo1
 4  {
 5      ///<summary>
 6     /// String连接测试
 7     /// </summary>
 8     public class Test
 9      {
10         public static void Main(string[] args)
11          {
12             string a = "1234";
13             Console.WriteLine(a);
14
15             a += "5678";
16             Console.WriteLine(a);
17             Console.ReadLine();
18         }
19     }
20 }
21
运行的结果:
 1234
 
 12345678
 
看起来我们似乎已经把MyStr的值从“1234”改为了“12345678”。事实是这样的吗?实际上并没有改变。在第5行代码中创建了一个String对象它的值是“1234”,MyStr指向了它在内存中的地址;第七行代码中创建了一个新的String对象它的值是“12345678”,MyStr指向了新的内存地址。这时在堆中其实存在着两个字符串对象,尽管我们只引用了它们中的一个,但是字符串“1234”仍然在内存中驻留。

二:引用类型

前面说过String是引用类型,这就是如果我们创建很多个相同值的字符串对象,它在内存中的指向地址应该是一样的。也就是说,当我们创建了字符串对象a,它的值是“1234”,当我们再创建一个值为“1234”的字符串对象b时它不会再去分配一块内存空间,而是直接指向了a在内存中的地址。这样可以确保内存的有效利用。看下面的代码:

 1 using System;
 2
 3 namespace Demo2
 4  {
 5      ///<summary>
 6     /// String引用类型测试
 7     /// </summary>
 8     public class Test
 9      {
10         public static void Main(string[] args)
11          {
12             string a = "1234";
13
14             Console.WriteLine(a);
15
16             Test.Change(a);
17
18             Console.WriteLine(a);
19             Console.ReadLine();
20         }
21
22         public static void Change(string s)
23          {
24             s = "5678";
25         }
26     }
27 }

运行结果:
 1234
 
 1234

做一个小改动,注意Change(ref string s)
 1 using System;
 2
 3 namespace Demo2
 4  {
 5      /// <summary>
 6     /// String引用类型测试
 7     /// </summary>
 8     public class Test
 9      {
10         public static void Main(string[] args)
11          {
12             string a = "1234";
13
14             Console.WriteLine(a);
15
16             Test.Change(ref a);
17
18             Console.WriteLine(a);
19             Console.ReadLine();
20         }
21
22         public static void Change(ref string s)
23          {
24             s = "5678";
25         }
26     }
27 }
28

运行结果:
 1234
 5678
 
 三:字符串的比较
 
在.NET中,对字符串的比较操作并不仅仅是简单的比较二者的值,==操作首先比较两个字符串的引用,如果引用相同,就直接返回True;如果不同再去比较它们的值。所以如果两个值相同的字符串的比较相对于引用相同的字符串的比较要慢,中间多了一步判断引用是否相同。看下面这段代码:

 1 using System;
 2
 3 namespace Demo3
 4  {
 5      /// <summary>
 6     /// String类型的比较
 7     /// </summary>
 8     public class Test
 9      {
10         public static void Main(string[] args)
11          {
12             string a = "1234";
13             string b = "1234";
14             string c = "123";
15             c += "4";
16
17             int times = 1000000000;
18             int start,end;
19            
20              ///测试引用相同所用的实际时间
21             start = Environment.TickCount;
22             for(int i=0;i<times;i++)
23              {
24                 if(a==b)
25                  {}
26             }
27             end = Environment.TickCount;
28             Console.WriteLine((end-start));
29            
30              ///测试引用不同而值相同所用的实际时间
31             start = Environment.TickCount;
32             for(int i=0;i<times;i++)
33              {
34                 if(a==c)
35                  {}
36             }
37             end = Environment.TickCount;
38             Console.WriteLine((end-start));
39
40             Console.ReadLine();
41         }
42     }
43 }
44
执行的结果(运行的结果可能有些不同):
 1671
 
 4172
由此我们看出值相同时的比较用= =比引用相同时的比较慢了好多。这里仅仅是一个测试,因为做这样的比较并没有任何实际的意义。
有一点需要明确的是,.NET中==跟Equals()内部机制完全是一样的,==是它的一个重载。
1 public static bool operator ==(string a, string b)
2  {
3       return string.Equals(a, b);
4 }
5

 1 public static bool Equals(string a, string b)
 2  {
 3       if (a == b)
 4        {
 5             return true;
 6       }
 7       if ((a != null) && (b != null))
 8        {
 9             return a.Equals(b);
10       }
11       return false;
12 }
13

 

四:字符串驻留

看一下这段代码:

 1 using System;
 2
 3 namespace Demo4
 4  {
 5      ///<summary>
 6     /// String的驻留
 7     /// </summary>
 8     public class Test
 9      {
10         public static void Main(string[] args)
11          {
12             string a = "1234";
13             string s = "123";
14             s += "4";
15
16             string b = s;
17             string c = String.Intern(s);
18
19             Console.WriteLine((object)a == (object)b);
20             Console.WriteLine((object)a == (object)c);
21             Console.ReadLine();
22         }
23     }
24 }
25
执行的结果:
 False
 
 True
在这段代码中,比较这两个对象发现它的引用并不是一样的。如果要想是它们的引用相同,可以用Intern()函数来进行字符串的驻留(如果有这样的值存在)。


三:StringBuilder对象
通过上面的分析可以看出,String类型在做字符串的连接操作时,效率是相当低的,并且由于每做一个连接操作,都会在内存中创建一个新的对象,占用了大量的内存空间。这样就引出StringBuilder对象,StringBuilder对象在做字符串连接操作时是在原来的字符串上进行修改,改善了性能。这一点我们平时使用中也许都知道,连接操作频繁的时候,使用StringBuilder对象。但是这两者之间的差别到底有多大呢?来做一个测试:

 1 using System;
 2 using System.Text;
 3
 4 namespace Demo5
 5  {
 6      ///<summary>
 7     /// String和StringBulider比较
 8     /// </summary>
 9     public class Test
10      {
11         public static void Main(string[] args)
12          {
13             string a = "";
14             StringBuilder s = new StringBuilder();
15
16             int times = 10000;
17             int start,end;
18            
19              ///测试String所用的时间
20             start = Environment.TickCount;
21             for(int i=0;i<times;i++)
22              {
23                 a += i.ToString();
24             }
25             end = Environment.TickCount;
26             Console.WriteLine((end-start));
27            
28              ///测试StringBuilder所用的时间
29             start = Environment.TickCount;
30             for(int i=0;i<times;i++)
31              {
32                 s.Append(i.ToString());
33             }
34             end = Environment.TickCount;
35             Console.WriteLine((end-start));
36
37             Console.ReadLine();
38         }
39     }
40 }
41

运行结果:
 884
 
 0

通过上面的分析,可以看出用String来做字符串的连接时效率非常低,但并不是所任何情况下都要用StringBuilder,当我们连接很少的字符串时可以用String,但当做大量的或频繁的字符串连接操作时,就一定要用StringBuilder。

 


                                                           
                                                           
                                                           
                                                             第二篇  进一步了解String


今天看到了你真的了解.NET中的String吗?的文章,写的很不错,对string不错的说明,但是有几点我想补充一下,一旦你的string在堆中创建后,其在内存中都是以const存在,任何的修改都会使其被重新创建为新的string,而指向以前的string的引用将会指向这个新的string!!

测试1:
看下面的代码:
1 string s = "1";
2 Console.WriteLine( String.IsInterned(s)!=null );//这里输出true

这个代码很简单,声名一个string s并且赋予"1",这个时候s在CLR的内置池中表示为引用,再来看下面的代码:
1 string s = "1";                //初始化string
2 Console.WriteLine( String.IsInterned(s)!=null );//这里输出true
3            
4 s += "2";                    //追加string
5 Console.WriteLine( String.IsInterned(s)!=null );//这里输出false
6

这个时候你再看输出的结果:第一次的s="1"的时候,s在内置池中,但是当你修改了s的值之后,它已经不在内置池中!!
如果你需要将s再次放置到内置池中,可以这么做:
1 string s = "1";                //初始化string
2 Console.WriteLine( String.IsInterned(s)!=null );//输出true
3            
4 s += "2";                    //追加string
5 Console.WriteLine( String.IsInterned(s)!=null );//输出false
6
7 String.Intern(s);            //重新设置为引用
8 Console.WriteLine( String.IsInterned(s)!=null );//这个时候依然输出为true

测试2:

看下面的测试代码:
 1 string a = "1";//第一次内置string
 2 a += "2";//第二次分配,赋值第一次的1到新的地址中,需重新分配内存
 3 a += "3";//第三次分配,赋值前两次的1,2到新的地址中,需重新分配内存
 4 a += "4";//第四次分配,赋值前三次的1,2,3到新的地址中,需重新分配内存
 5
 6  /*  使用IL反编译后看的结果
 7  *
 8  * .method private hidebysig static void  Main(string[] args) cil managed
 9 {
10   .entrypoint
11   .custom instance void [mscorlib]System.STAThreadAttribute::.ctor() = ( 01 00 00 00 )
12   // 代码大小       43 (0x2b)
13   .maxstack  2
14   .locals init ([0] string a)
15   IL_0000:  ldstr      "1"
16   IL_0005:  stloc.0
17   IL_0006:  ldloc.0
18   IL_0007:  ldstr      "2"
19   IL_000c:  call       string [mscorlib]System.String::Concat(string,//注意这里,复制一次
20                                                               string)
21   IL_0011:  stloc.0
22   IL_0012:  ldloc.0
23   IL_0013:  ldstr      "3"
24   IL_0018:  call       string [mscorlib]System.String::Concat(string,//复制二次
25                                                               string)
26   IL_001d:  stloc.0
27   IL_001e:  ldloc.0
28   IL_001f:  ldstr      "4"
29   IL_0024:  call       string [mscorlib]System.String::Concat(string,//复制第三次
30                                                               string)
31   IL_0029:  stloc.0
32   IL_002a:  ret
33 } // end of method Class1::Main
34  *
35

相信通过上面的代码和反编译后的结果,大家可以看的很清楚,string如何被分配,而在你重新修改string的时候,是如何工作..既然发现了问题,当然也可以解决问题所在..解决和优化的办法很多,我只简单的列出几种,第一种,使用string[]数组来代替..看下面代码:
 1 string[] Arr1 = new string[4];//声名需要内置4个string
 2 Arr1[0] = "1";//内置了1
 3 Arr1[1] = "2";//内置了2
 4 Arr1[2] = "3";//内置了3
 5 Arr1[3] = "4";//内置了4
 6  /*数组赋值后,在IL反编译后的表现
 7  *
 8  *
 9  *.method private hidebysig static void  Main(string[] args) cil managed
10 {
11   .entrypoint
12   .custom instance void [mscorlib]System.STAThreadAttribute::.ctor() = ( 01 00 00 00 )
13   // 代码大小       40 (0x28)
14   .maxstack  3
15   .locals init ([0] string[] Arr1)
16   IL_0000:  ldc.i4.4
17   IL_0001:  newarr     [mscorlib]System.String
18   IL_0006:  stloc.0
19   IL_0007:  ldloc.0
20   IL_0008:  ldc.i4.0
21   IL_0009:  ldstr      "1"
22   IL_000e:  stelem.ref
23   IL_000f:  ldloc.0
24   IL_0010:  ldc.i4.1
25   IL_0011:  ldstr      "2"
26   IL_0016:  stelem.ref
27   IL_0017:  ldloc.0
28   IL_0018:  ldc.i4.2
29   IL_0019:  ldstr      "3"
30   IL_001e:  stelem.ref
31   IL_001f:  ldloc.0
32   IL_0020:  ldc.i4.3
33   IL_0021:  ldstr      "4"
34   IL_0026:  stelem.ref
35   IL_0027:  ret
36 } // end of method Class1::Main
37  *
38  * */

看看上面的代码,我想很明白,string[]是如何工作的了吧??可以这么理解:每个数组的子项都是一个被内置的string!!
第二种解决办法是char[],如果你知道你的字符串大小,可以这么写char[] c = new char[4]{'1','2','3','4'};这个做法在C/C++中是很不错的,但是在C#似乎用的不多,而且用起来也比较麻烦.因为它不能想C/C++这样: char[] c = {"1234"};不过我依然在做一个加密/解密类的时候用了char[]!!
最后一种也是最常用的:StringBuilder,既然string有重新分配地址的副作用.所以微软也为我们提供了StringBuilder来解决这个问题..

 

 

                                                         
                                                         
                                                         
                                                           第三篇: StringBuilder的实现与技巧


在上一篇进一步了解String 中,发现了string的不便之处,而string的替代解决方案就是StringBuilder的使用..它的使用也很简单System.Text.StringBuilder sb = new System.Text.StringBuilder();这样就初始化了一个StringBuilder ..之后我们可以通过Append()来追加字符串填充到sb中..在你初始化一个StringBuilder 之后,它会自动申请一个默认的StringBuilder 容量(默认值是16),这个容量是由Capacity来控制的.并且允许,我们根据需要来控制Capacity的大小,也可以通过Length来获取或设置StringBuilder 的长度..
先来看Length的用法:
1 System.Text.StringBuilder sb = new System.Text.StringBuilder();
2 sb.Append( "123456789" );//添加一个字符串
3 sb.Length = 3;//设置容量为3
4 Console.WriteLine( sb.ToString() );//这里输出:123
5
6 sb.Length = 30;//重新设置容量为30
7 Console.WriteLine( sb.ToString() + ",结尾");//这里在原来字符串后面补齐空格,至到Length的为30
8 Console.WriteLine( sb.Length );//这里输出的长度为30

通过上面的代码,我们可以看出如果StringBuilder 中的字符长度小于Length的值,则StringBuilder 将会用空格硬填充StringBuilder ,以满足符合长度的设置..如果StringBuilder 中的字符长度大于Length的值,则StringBuilder 将会截取从第一位开始的Length个字符..而忽略超出的部分..

再来看看最重要的部分Carpacity的用法:
 1 System.Text.StringBuilder sb = new System.Text.StringBuilder();//初始化一个StringBuilder
 2 Console.Write( "Capacity:" + sb.Capacity );//这里的Capacity会自动扩大
 3 Console.WriteLine( "/t Length:" + sb.Length );
 4
 5 sb.Append( '1',17 );//添加一个字符串,这里故意添加17个字符,是为了看到Capacity是如何被扩充的
 6 Console.Write( "Capacity:" + sb.Capacity );//这里的Capacity会自动扩大
 7 Console.WriteLine( "/t Length:" + sb.Length );
 8
 9 sb.Append( '2',32 );//添加一个字符串
10 Console.Write( "Capacity:" + sb.Capacity );//这里的Capacity会自动扩大
11 Console.WriteLine( "/t Length:" + sb.Length );
12
13 sb.Append( '3',64 );//添加一个字符串
14 Console.Write( "Capacity:" + sb.Capacity );//这里的Capacity会自动扩大
15 Console.WriteLine( "/t Length:" + sb.Length );
16
17 //注意这里:如果你取消Remove这步操作,将会引发ArgumentOutOfRangeException异常,因为当前容量小于
18
19 //Length,这在自己控制StringBuilder的时候务必要注意容量溢出的问题
20
21 sb.Remove(0,sb.Length);//移出全部内容,再测试
22 sb.Capacity = 1;//重新定义了容量
23 sb.Append( 'a',2 );
24 Console.Write( "Capacity:" + sb.Capacity );//这里的Capacity会自动扩大
25 Console.WriteLine( "/t Length:" + sb.Length );
26
27 sb.Append( 'b',4 );
28 Console.Write( "Capacity:" + sb.Capacity );//这里的Capacity会自动扩大
29 Console.WriteLine( "/t Length:" + sb.Length );
30
31 sb.Append( 'c',6 );
32 Console.Write( "Capacity:" + sb.Capacity );//这里的Capacity会自动扩大
33 Console.WriteLine( "/t Length:" + sb.Length

上面的代码输出的结果:
1 Capacity:16     Length:0    //输出第一次,默认的Capacity是16
2 Capacity:32     Length:17    //第二次,我们故意添加了17个字符,于是Capacity=Capacity*2
3 Capacity:64     Length:49    //继续超出,则Capacity=Capacity*2
4 Capacity:128     Length:113
5 Capacity:3     Length:2    //清空内容后,设置Capacity=1,重新添加了字符
6 Capacity:7      Length:6    //后面的结果都类似
7 Capacity:14     Length:12

从上面的代码和结果可以说明StringBuilder中容量Capacity是如何增加的:创建一个StringBuilder之后,默认的Capacity初始化为16,接着我们添加17个字符,以方便看到Capacity的扩充后的值..大家在修改Capacity的时候,一定要注意21行的注释,一定要确保Capacity >= Length,否则会引发ArgumentOutOfRangeException异常...看完结果,就可以推断出Capacity的公式:
if ( Capacity < Length && Capacity > 0 ){
      Capacity *= 2;
}
OK..看到公式就明白了..StringBuilder是以当前的Capacity*2来扩充的..所以,在使用StringBuilder需要特别注意,尤其是要拼接或追加N多字符的时候,要注意技巧的使用,可以适当的,有预见性的设置Capacity的值,避免造成过大内存的浪费,节约无谓的内存空间..例如,下列代码就可以根据情况自动的扩展,而避免了较大的内存浪费.
 1 System.Text.StringBuilder sb = new System.Text.StringBuilder();
 2 int i = 0;
 3 long StartTime  = DateTime.Now.Ticks;
 4  while ( i < 100000 ) {
 5 sb.Append( i.ToString() );
 6 i++;
 7 }
 8 long EndTime  = DateTime.Now.Ticks;
 9
10 Console.WriteLine( "时间:" + ( EndTime-StartTime ) + "/t Capacity:"+ sb.Capacity + "/t Length:"
11
12 + sb.Length);
13
14 System.Text.StringBuilder sb1 = new System.Text.StringBuilder();
15 i = 0;
16 StartTime  = DateTime.Now.Ticks;
17 while ( i < 100000 )
18  {
19 if ( sb1.Capacity <= sb1.Length )//先判断是否>Length
20 sb1.Capacity += 7;//这里一定要根据情况的增加容量,否则会有性能上的消耗
21 sb1.Append( i.ToString() );
22 i++;
23 }
24 EndTime  = DateTime.Now.Ticks;
25
26 Console.WriteLine( "时间:" + ( EndTime-StartTime ) + "/t Capacity:"+ sb1.Capacity + "/t
27
28 Length:" + sb1.Length);
需要特别说明的一点是,自动增加的容量,一定要根据实际预见的情况而改变,否则不但起不到优化的作用,反而会影响到程序的性能..


另外,如果有时间的话,可以测试一下下面的代码,用string和StringBuilder拼接字符串的区别..你会吓到的!!
 1 System.Text.StringBuilder sb = new System.Text.StringBuilder();
 2 int i = 0;
 3 long StartTime  = DateTime.Now.Ticks;
 4  while ( i < 100000 ) {
 5 sb.Append( i.ToString() );
 6 i++;
 7 }
 8 long EndTime  = DateTime.Now.Ticks;
 9
10 Console.WriteLine( "时间:" + ( EndTime-StartTime ) );
11
12 string sb1 = null;
13 i = 0;
14 StartTime  = DateTime.Now.Ticks;
15 while ( i < 100000 )
16  {
17 sb1 += i;
18 i++;
19 }
20 EndTime  = DateTime.Now.Ticks;
21 Console.WriteLine( "时间:" + ( EndTime-StartTime ));




Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=729295

你可能感兴趣的:([mqt_2003的专栏]String和StringBuilder(C#))