string作为我们在编程当中用的最多的数据类型,同时又由于它的特殊性,怎么强调它的重要性都不为过,理解string的一些类型和存储机制,有助于我们写出正确且高效的代码.一.string类型 1.string的类型 string类型直接继承Object类型,Object类型是引用类型,因而string类型是引用类型无疑. 我们借助VS的类视图可以看到这一点: 这意味着: (a).string类型不会在线程的堆栈中存储任何字符串,而是存储在堆上 (b).未初始时,它被设置为null PS:在内部,string是用字符串char的集合来维护的 2.string声明的IL描述 在IL中,构造新实例的IL指令是newobj,是不是string也是这样? 我们使用如下代码: 1 class Program 2 { 3static void Main(string[] args) 4 { 5string str = "Hello World!"; 6string str2 = "Hello" + " My" + " World!"; 7 Person person = new Person(); 8} 9}10 11 class Person12 {13 string Name;14 }我们查看IL代码如下: 可以看出 (a).对比1和3,构造Person对象使用了newobj指令,但是在构造字符串的时候,使用了专门的ldstr(load string)指令 (b).更进一步,编译器将这些字面值字符串放到模块的元数据中,在运行时加载和引用它们 (c).看2,对于使用+符合将各literal连接起来的写法,编译器在编译的过程中会直接连接他们.二.string的操作带来的疑问 OK,通过第1部分,我们知道了,string是引用类型,它存储在堆中. 我们知道对于引用类型,赋值操作=会传递的是引用,不是值,但构造不同的引用类型时通常它们的引用也不同.如下面这种: 1 class Program 2 { 3static void Main(string[] args) 4{ 5//Person实害?例 6Person person1 = new Person("A"); 7Person person2 = new Person("A"); 8Console.WriteLine(object.ReferenceEquals(person1, person2)); 9 10//string11string str1 = "Hello World!";12string str2 = "Hello World!";13string str3 = "Hello " + "World!";14Console.WriteLine(object.ReferenceEquals(str1, str2));15Console.WriteLine(object.ReferenceEquals(str1, str3));16 17Console.Read();18}19 }20 21class Person22 {23public Person(string strName)24{25 26}27 } 我们先给出运行结果: 我们知道object.ReferenceEquals是比较两个对象的引用是否一样,对于第1种Person的情况,我们可以理解,因为他们都是构造了不同的对象,引用的存储地址也是不同的.但对于第2种,第3种,string就像成为了值类型一样,返回了True,那么问题来了: A.在声明的时候,string存储的是什么? B.什么原因使得两个string的引用地址是一样的? 这就引出了我们要讨论的核心问题:字符串驻留.三.字符串驻留 1.string存储的是引用 string对象存储的是引用,引用对象存储在堆中,会生成一个对象,同时将这个对象的地址(引用)给堆栈去使用.也就是说两个string引用了堆中同一块对象. 2.字符串驻留让两个string的引用地址是一样 在CLR初始化时,会创建一个Hash表,在这个表中,Key是字符串,值是字符串在堆中的地址.当声明一个字符串的时候,会先去这个HashTable中去找是否存在这个Key,如果存在则返回对应的引用,如果不存在则纳入HashTable.如下图所示: Step1:当执行语句string str1 = "Hello World!";时,str1拿到了Add1; Step2:当执行语句string str2= "Hello World!";时,CLR会去HashTable中去找,找到,返回Add1给str2; Step3:现在用object.ReferenceEquals比较str1和str2的引用,因为都是Add1,因而返回True. 我们现在通过内存分析工具ANTS Memory Profile来证明,字符串驻留机制是确实存在的. 代码如下:1 static void Main(string[] args)2 {3Console.ReadLine();//第台?一?次?快ì照?位?置?4string str1 = "Hello World!";5string str2 = "Hello World!";6Console.ReadLine();//第台?二t次?快ì照?位?置?7 } 加载两次快照,对比差异: 我们可以看到,在这里有一个string的实例进去了,而且整个过程当中,也只有这一个string实例进去了,我们可以进一步看下进去的内容是什么. 我们在这里发现了”Hello World!”字符串,并且只有一个.这也就从内存分析的角度证明了字符串驻留的存在. 3.驻留字符串的HashTable是不受GC管理,但表达式中存在variable时,则不驻留在HashTable 我们实验如下: 1 static void Main(string[] args) 2 { 3Console.ReadLine();//第1次快照位置 4Test(); 5GC.Collect(); 6Console.ReadLine();//第3次快照位置 7 } 89 static void Test()10 {11string str1 = "Hello World!";12string str2 = "Hello World!" + str1;13 Console.ReadLine();//第2次快照位置14 } 第2次快照,我们可以看到: 进去了3个对象,分别是:byteIndex,”Hello World!”,”Hello World!Hello World!” 第3次快照是在调用了GC.Collect()后再进行的快照,以快照2为对比线,我们查看第3次快照. 我们看到,有一个对象被GC回收掉了,具体是什么被回收了?我们再看: 现在只剩下byteIndex,”Hello World!”两个对象,什么被回收了呢?显然是:”Hello World!Hello World!” 这也就证明了我们所说的:驻留字符串的HashTable是不受GC管理,但表达式中存在variable时,则不驻留在HashTable. 进一步:除非卸载AppDomain或进程终止,否则HashTable引用的string对象不能被释放. 4.字符串的驻留是基于整个进程的 我们添加两个不同的AppDomain,在各自的应用程???域中执行BuildString()方法,同时由于应用程序域之间本是不能访问彼此对象的,我们使用"封送(Marshaling)"机制,封送又分为按值分送(主要采用序列化的方式)和按引用封送(如采用.Net Remoting).这里,要实现按引用封送,Test类继承MarshalByRefObject类. 测试代码class Program{ static void Main(string[] args) { Console.ReadLine();AppDomain domina1 = AppDomain.CreateDomain("First");Test t1 = (Test)domina1.CreateInstanceAndUnwrap(typeof(Test).Assembly.FullName, typeof(Test).FullName);t1.BuildString();AppDomain domina2 = AppDomain.CreateDomain("Second");Test t2 = (Test)domina1.CreateInstanceAndUnwrap(typeof(Test).Assembly.FullName, typeof(Test).FullName);t2.BuildString();Console.ReadLine();}}public class Test : MarshalByRefObject{ public void BuildString() {var str1 = "Hello";var str2 = "Hello";var str3 = "World";var str4 = "World"; }} 我们拿到两张快照,在第1张跟第2张快照对比后我们发现: 我们再具体查看内容(“World”字符串就不截图了): 通过以上的分析,我们确信,字符串的驻留是基于整个进程的. 5.我们可以通过string.Intern方法来将字符串强制加入HashTable,也可以通过string.IsInterned来判断字符串是否在HashTable中存在。四.字符串池 在编译时,编译器会处理所有的literal字符串,并嵌入托管模块的元数据中,但如果每次都写入元数据,假设这个字符串在程序中多次出现,那就需要多次写入元数据,这会使生成的文件无限地增大. C#编译器,只在元数据中将literal字符串写入一次,将多个实例合并成一个实例,所有引用该字符串的代码都被修改成引用元数据中的同一个字符串,这能显著地减少生成文件的大小.这种特性,我们称之为字符串池.五.string的不可变性 string是不可变的,这意味着: a.字符串一经创建便不能更改,不能变长、变短或修改其中的任何字符; b.每次对于字符串的变更操作,如果是带变量操作,都会在堆上生成新的字符串,并返回新的引用,会造成频繁的GC回收,从而造成性能问题,如果不带变量操作则会采用字符串驻留; c.操作和访问字符串不会发生线程同步问题,线程安全; d.String类是sealed(密封)的,这是为了保护string的不可变性。 问题来了,如何实现string的不可变性呢? string在内部是用char数组实现的,在char数据中,我们不可以改变数组的引用,但是我们可以直接修改char数组的值,为了实现string的不可变性,string在实现各种方法时,不会触动char数组中的元素。 参见7.六.StringBuilder:为解决string的性能而生 通过前面的内容我们可以知道,string容易产生性能问题,StringBuilder可以解决这个问题。 它的内部使用char[]来进行操作,默认为16,如果超过容量,则在堆中产生一个倍增容易的新char[]数组,复制字符,并开始使用新数组,前一个数组则被GC回收。如果不超过当前容量,是不是会产生一个新的char[]数组的。 使用ToString()方法也会在堆中产生一个新的对象。七.总结 1.string是引用类型 2.string使用了字符串池来减少元数据文件的大小 3.string使用了字符串驻留来提升效率,驻留的字符串采用HashTable来存储,它不受GC管辖,HashTable是基于进程共享的. 4.string是不可变的,由此带来的性能问题,可以通过StringBuilder来解决.本文永久更新链接地址:http://www.linuxidc.com/Linux/2016-11/137000.htm