相信有很多开发人员都有这样的面试经历:面试官就某个问题对你追着问,不仅问你是什么,还要问你为什么以及它的内部机制,直至他认为你把问题阐述的非常透彻才肯罢手,这就要求我们的开发人员对这些问题要做到深刻的理解。正是基于此,才有了本篇随笔的产生,在这篇文章里我将着重阐述我对String对象的理解,例如String的类型,它的内存分配模型以及它适合在什么情况下使用等等。
String VS string
其实二者的作用是一样的,之所以说它们是一样的,是因为在编译的时候,CLR在其内部使用了using string = System.String这样一个表达式,换句话说string就代表了String,或者说string是String的一个别名,只不过需要注意的是前者是C#的一个对象,而后者是C#的一个关键字,C#中类似的关键字还有例如int, bool, float等等。
String之类型
String是一个引用类型,虽然其行为看起来像是一个值类型,下面将通过一个Sample来说明,为此我们先建一个Console应用程序如下:

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

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
String内存分配模型
虽然String是引用类型,但是其行为和一般引用类型的行为根本不一样,相反倒是和值类型很相似,例如当我们试图改变某个字符串变量的值,可是引用这个变量的其他字符串的值根本不会改变,这到底是怎么回事呢?为了更好地说明问题的本质,下面我将再次通过一个例子来说明:
同样,首先我们建立一个用于测试的Console应用程序:
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代码之后,不用我说你肯定能知道其运行结果以及为什么是这样的结果:

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)由于具有相同值的字符串在会在表中保存一次,这就保证了在使用时的一致性。
2 -------------------------------------------------------
3 70000001 : (16) L"This is a string"
4 70000023 : (22) L"This is another string"