全面剖析C#之String对象

  相信有很多开发人员都有这样的面试经历:面试官就某个问题对你追着问,不仅问你是什么,还要问你为什么以及它的内部机制,直至他认为你把问题阐述的非常透彻才肯罢手,这就要求我们的开发人员对这些问题要做到深刻的理解。正是基于此,才有了本篇随笔的产生,在这篇文章里我将着重阐述我对String对象的理解,例如String的类型,它的内存分配模型以及它适合在什么情况下使用等等。

String VS string

 其实二者的作用是一样的,之所以说它们是一样的,是因为在编译的时候,CLR在其内部使用了using string = System.String这样一个表达式,换句话说string就代表了String,或者说string是String的一个别名,只不过需要注意的是前者是C#的一个对象,而后者是C#的一个关键字,C#中类似的关键字还有例如int, bool, float等等。

String之类型

String是一个引用类型,虽然其行为看起来像是一个值类型,下面将通过一个Sample来说明,为此我们先建一个Console应用程序如下:

ExpandedBlockStart.gif ConsoleApplication_C#
 1  using  System;
 2 
 3  namespace  ConsoleApplication_CSharp
 4  {
 5       class  Program
 6      {
 7           static   void  Main( string [] args)
 8          {
 9               int  i  =   100 ;
10              Console.WriteLine(i);
11               string  str1  =   " This is a string " ;
12              Console.WriteLine(str1);
13               string  str2  =   " Hello, "   +  str1;
14              Console.WriteLine(str2);
15              Console.ReadKey();
16          }
17      }
18  }

下面我们再来看一下生成的IL代码(使用MS自带的ILDASM.exe):

ExpandedBlockStart.gif ConsoleApplication_IL
 1  .method   private   hidebysig  static  void   Main( string [] args)  cil   managed
 2  {
 3     .entrypoint
 4     //  Code size       50 (0x32)
 5     .maxstack    2
 6     .locals   init  ([ 0 int32  i,
 7             [ 1 string  str1,
 8             [ 2 string  str2)        --声明所有的变量
 9     IL_0000:    nop                     --如果修补操作码,则填充空间。尽管可能消耗处理周期,但未执行任何有意义的操作
10     IL_0001:    ldc.i4.s     100         --将提供的int8值即100作为int32推送到计算堆栈上(短格式)
11     IL_0003:    stloc.0                 --从堆栈的顶部弹出值并将其付给内存中第一个变量i
12     IL_0004:    ldloc.0                 --将内存变量i的值压入堆栈
13     IL_0005:    call         void  [mscorlib]System.Console::WriteLine( int32 )--调用WriteLine方法,参数为栈顶的值,即100
14     IL_000a:    nop
15     IL_000b:    ldstr        " This is a string " --推送对元数据中存储的字符串的新对象引用并压入堆栈中
16     IL_0010:    stloc.1                 --从堆栈的顶部弹出值并将其付给内存中第二个变量str1
17     IL_0011:    ldloc.1                 --将内存变量str1的值压入堆栈
18     IL_0012:    call         void  [mscorlib]System.Console::WriteLine( string )--调用WriteLine方法,参数为栈顶的值,即This is a  string
19     IL_0017:    nop
20     IL_0018:    ldstr        " Hello, " --推送对元数据中存储的字符串的新对象引用并压入堆栈中
21     IL_001d:    ldloc.1                --将内存变量str1的值压入堆栈
22     IL_001e:    call         string  [mscorlib]System.String::Concat( string ,
23                                                                 string )--调用Concat方法,参数分别为Hello,和This is a  string
24     IL_0023:    stloc.2                --从堆栈的顶部弹出值并将其付给内存中第三个变量str2,此时str2值为Hello,This is a  string
25     IL_0024:    ldloc.2
26     IL_0025:    call         void  [mscorlib]System.Console::WriteLine( string )
27     IL_002a:    nop
28     IL_002b:    call        valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
29     IL_0030:    pop
30     IL_0031:    ret
31  //  end of method Program::Main
我已经对上面的代码做了详细的注释,读者应该非常容易理解,从中我们不难看出int和string的差别,对于int类型,IL是这样处理的:
IL_0001:  ldc.i4.s   100
只是简单地把值100推送到计算堆栈,而string的处理却大不一样:
IL_000b:  ldstr      "This is a string"
翻看IL的语法,我们知道ldstr的含义是 Pushes a new object reference to a string literal stored in the metadata.用我们的大白话就是先在托管堆中创建了一个字符串对象,对象的值就是存储在元数据中的对应的字符串,然后把这个对象的引用压入计算堆栈中,至此,你应该知道string是引用类型了吧。
说到这,估计有人就会犯嘀咕了,既然string是引用类型,那么当改变一个字符串的值,为什么引用这个变量的其他字符串的值不会跟着改变,这就得从string的内存分配模型说起了。

 String内存分配模型

虽然String是引用类型,但是其行为和一般引用类型的行为根本不一样,相反倒是和值类型很相似,例如当我们试图改变某个字符串变量的值,可是引用这个变量的其他字符串的值根本不会改变,这到底是怎么回事呢?为了更好地说明问题的本质,下面我将再次通过一个例子来说明:

同样,首先我们建立一个用于测试的Console应用程序:

 1  static   void  Main( string [] args)
 2  {
 3       string  str1  =   " This is a string " ;
 4       string  str2  =  str1;
 5      Console.WriteLine(str1  ==  str2);
 6      str1  =   " This is another string " ;
 7      Console.WriteLine(str1);
 8      Console.WriteLine(str2);
 9      Console.WriteLine(str1  ==  str2);
10      Console.ReadKey();
11  }

按道理说,我现在应该给出程序的运行结果,可是别急,还是让我先来分析一下编译之后生成的IL代码,我相信在看完IL代码之后,不用我说你肯定能知道其运行结果以及为什么是这样的结果:

ExpandedBlockStart.gif 代码
 1  .method   private   hidebysig  static  void   Main( string [] args)  cil   managed
 2  {
 3     .entrypoint
 4     //  Code size       62 (0x3e)
 5     .maxstack    2
 6     .locals   init  ([ 0 string  str1,
 7             [ 1 string  str2)--初始化所有变量
 8     IL_0000:    nop
 9     IL_0001:    ldstr        " This is a string " --推送新对象引用至栈中
10     IL_0006:    stloc.0         --取栈顶值并赋予内存变量str1
11     IL_0007:    ldloc.0         --取内存变量str1值并入栈
12     IL_0008:    stloc.1         --取栈顶值并赋予内存变量str2
13     IL_0009:    ldloc.0         --取内存变量str1值并入栈
14     IL_000a:    ldloc.1         --取内存变量str2值并入栈
15     IL_000b:    call         bool  [mscorlib]System.String::op_Equality( string ,
16                                                                    string )--调用方法比较str1和str2
17     IL_0010:    call         void  [mscorlib]System.Console::WriteLine( bool )--输出比较结果
18     IL_0015:    nop
19     IL_0016:    ldstr        " This is another string " --推送新对象引用至栈中
20     IL_001b:    stloc.0         --取栈顶值并赋予内存变量str1
21     IL_001c:    ldloc.0         --取内存变量str1值并入栈
22     IL_001d:    call         void  [mscorlib]System.Console::WriteLine( string )--输出str1
23     IL_0022:    nop
24     IL_0023:    ldloc.1         --取内存变量str2值并入栈
25     IL_0024:    call         void  [mscorlib]System.Console::WriteLine( string )--输出str2
26     IL_0029:    nop
27     IL_002a:    ldloc.0         --取内存变量str1值并入栈
28     IL_002b:    ldloc.1         --取内存变量str2值并入栈
29     IL_002c:    call         bool  [mscorlib]System.String::op_Equality( string ,
30                                                                    string )--调用方法比较str1和str2
31     IL_0031:    call         void  [mscorlib]System.Console::WriteLine( bool )--输出比较结果
32     IL_0036:    nop
33     IL_0037:    call        valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
34     IL_003c:    pop
35     IL_003d:    ret
36  //  end of method Program::Main

上面的IL代码说明几个问题,下面将一一加以解释。

第一,为什么改变str1的值不会影响str2的值:

给str1第一次初始化赋值(string str1 = "This is a string";)的IL代码是IL_0001:  ldstr      "This is a string"--推送新对象引用至栈中,

而后来改变str1值(str1 = "This is another string";)的IL代码是IL_0016:  ldstr      "This is another string"--推送新对象引用至栈中,

显然,原来每次赋值或者说改变str1的值,都会导致新对象(String)的创建,所以说无论怎么改变str1的值都不会影响str2,因为你改变str1相当于创建了一个全新的String对象,和str2一点关系都没有,另外,这也解释和说明了String是不可变的(所以说我们想‘改变’字符串值的美好愿望是徒劳的),进一步地,也告诉我们为什么不能频繁地改变String的值,因为这将导致String对象的频繁创建与与销毁(GC),这对性能是一个极大的损耗。

第二,为什么语句string str2 = str1;不会导致新对象的创建:

IL_0008:  stloc.1  --取栈顶值并赋予内存变量str2

仅仅是把str1取出来然后赋给str2,这又是为什么呢?原来CLR为了提高String的使用效率,对其使用了字符串驻留/拘留技术,即在程序编译的时候,CLR就会收集所有用到的字符串变量的值并把其放入元数据中,然后在内存中创建了一张用于维护这些字符串的散列表,键值分别为字符串的值和对象在托管堆中的引用,这样做有两个好处,1)下次如果需要创建新的字符串,CLR会先检查这个字符串的值在表中是否存在,如果存在,就不会创建新的字符串对像,而只是使字符串引用到对应的键值对;如果不存在才会创建,这样做极大地提高了字符串的使用效率;2)由于具有相同值的字符串在会在表中保存一次,这就保证了在使用时的一致性。

1  User Strings
2  -------------------------------------------------------
3  70000001  : ( 16 ) L " This is a string "
4  70000023  : ( 22 ) L " This is another string "

转载于:https://www.cnblogs.com/panchunting/archive/2011/05/26/CSharp_String.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值