由表及里,由感性到理性,我们先来看一看string类的Copy-On-Write的表面特征。我写了下面一段程序:
2 {
3 static void Main(string[] args)
4 {
5 Console.WriteLine("Sharing the Memory:");
6 string str1 = "Hello World";
7 string str2 = str1;
8 Console.WriteLine(str1);
9 Console.WriteLine(str2);
10
11 Console.WriteLine("After Copy on Write:");
12 str1 = str1.Replace('H', 'C');
13 Console.WriteLine(str1);
14 Console.WriteLine(str2);
15
16 Console.ReadLine();
17 }
18 }
执行结果:Sharing the Memory:
Hello World
Hello World
After Copy on Write:
Cello World
Hello World
从执行结果我们看到,在没有修改之前,str1和str2的值是一样的,也就是说他们在内存中存放数据的地址是一样的。str1修改之后,他的值发生了变化,即存放数据的地址改变了。而str2没有发生变化。在这里我们就看到了string的Copy-on-Write技术的影子。
我们再深入的分析,在string中实现的写时拷贝,要解决两个问题:一个是内存共享,一个是Copy-on-Write。
Copy-on-Write的原理:它使用的是“引用计数”,当第一个类构造时,string类的构造函数根据传入的参数在堆上分配内存,当其它类需要这块内存时,这个计数自动累加。当灯析构时,这个计数减一,直到最后一个类析构时,这个计数为0,这时,程序才会真正释放这块内存。“引用计数”就是string类的Copy-on-Write的原理。
内存共享:如果一个类要用另一个类的数据,那就可以共享被使用类的内存了。这是很合理的,如果你不用我的,那就不用共享,只有你使用我的,才发生共享。
使用别的类的数据时,无非两种情况:(1)用别的类构造自己。(2)用别的类赋值。第一种情况是在构造函数中引用计数累加,第二种情况是在赋值操作中引用计数累加。
那么这个引用计数应该存放在哪里呢?它是存放在共享内存中的,在这儿存放着所有的引用。这样一来,所有共享一块内存区的类都有同样的一个引用计数,而这个变量的地址既然是在共享区上的,那么所有共享这块内存的类都可以访问到,也就知道这块内存的引用者有多少了。
有了这样一个机制,每当我们为string分配内存时,我们总是要多分配一个空间用来存放这个引用计数的值,只要发生拷贝构造或是赋值时,这个内存的值就会加一。而在内容修改时,string类为查看这个引用计数是否为0,如果不为零,表示有人在共享这块内存,那么自己需要先做一份拷贝,然后把引用计数减去一,再把数据拷贝过来。
就我们上面的那个例子来说:当string str1="Hello World"时,在分配内存时就多分配一个空间用来存放这个引用计数的值,当string str2=str1时,str1发现有人引用我,它就得共享自己。并将引用计数加1。当str1=str1.replace('H','C')时,它首先去查看引用计数,一看大于0,表示有人在共享,那么自己需要先做一份拷贝,然后把引用计数减去一,再把数据拷贝过来。所以,我们在修改之后,再看他们的值时,str1变了,而str2并没有变。
现在我们再来回头说说这个Copy-on-Write,我们可以把它看作是一个虚拟代理,它代理的真正的对象是共享内存,而代理完成任务就是控制要引用共享数据对象,它来判断是否有在共享这个数据,即看看引用计数是否0.若为0,表示数据没有人在共享,可以直接修改;若不为0,表示有人在共享这个数据,它就不能直接修改,需要Copy一个副本,在副本的基础上去修改。
最后将它改成1231234。把新实例的地址直接赋str
我们是使用了stringbuilder这个代理类来访问string的。