• 全面剖析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应用程序如下:

    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):

    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 ([0int32 i,
     7            [1string str1,
     8            [2string 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代码之后,不用我说你肯定能知道其运行结果以及为什么是这样的结果:

    代码
     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 ([0string str1,
     7            [1string 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"
  • 相关阅读:
    基于NS2的差分服务网络测试(含awk分析代码)
    ubuntu14下NSG2的安装和使用
    (转)QOS入门详解
    gnuplot安装的小问题
    中介者模式(Mediator)_java实现
    命令模式(Command)_java实现
    观察者模式(Observer)_java实现
    策略模式(Strategy)_java实现
    状态模式(State)_java实现
    备忘录模式(Memento)_java实现
  • 原文地址:https://www.cnblogs.com/panchunting/p/CSharp_String.html
Copyright © 2020-2023  润新知