System.String 是 C# 基础类型中唯一的引用类型。但是,它却具有很多值类型的特点。
我们来看一段简单的代码:
string text = "Red";
string _tempStr = text;
_tempStr = "Blue";
Console.WriteLine(text);
Console.ReadKey();
按照引用的理论,此处 _tempStr 变量应该是存储的 text 变量的地址,那么修改 _tempStr 变量的值,text 的值就应该随之改变。
那么,此时 text 变量的值应该就是 "Blue",但事实上经过测试 text 变量的值还是 "Red"。
那就说明 _tempStr 变量肯定不是存储的 text 变量的地址。但,这样又违背了它是引用类型的这一特点,那它的内部究竟是怎么样处理的呢?
据我了解,微软应该是在 String 类型中引入了 Copy-On-Write(写时拷贝) 技术,先来简要说明一下什么是 Copy-On-Write 技术:
简单来说,在复制一个对象时并不是真的在内存中把原来对象的数据复制一份到另外一个地址,而是在新对象的内存映射表中指向同原对象相同的位置,
并且把那块内存的 Copy-On-Write 位设为 1。在对这个对象执行读操作的时候,内存数据没有变动,直接执行就可以。
在写的时候,才真正将原始对象复制一份到新的地址,修改新对象的内存映射表到这个新的位置,然后往这里写。
有一定经验的程序员应该都知道,Copy-On-Write(写时拷贝) 技术使用了 "引用计数" 方式,会有一个变量用于保存引用的数量。
当第一个类构造时,String 的构造函数会根据传入的参数从堆上分配内存,当有其它类需要这块内存时,这个计数为自动累加。
当有类析构时,这个计数会减一,直到最后一个类析构时,此时的引用计数为 1 或是 0,此时,程序才会真正的释放这块从堆上分配的内存。
说白了,"引用计数" 就是 String 类中写时拷贝的原理!
事实上,String 还是一个不可变的数据类型,一旦对 String 类型的对象进行了初始化,该字符串对象就不能改变了。
为了说明这一点,我们再来看一小段很简单的代码:
string name = "XXXXXX";
name += " is a gentleman.";
在执行这段代码时,首先创建一个名为 name 的 String 类型的对象,并初始化为 "XXXXXX"。
此时,.NET 运行库会为该字符串分配足够的内存来保存这个文本,然后,再设置变量 name,来表示这个字符串实例。
从语法上看,第二行代码是将更多的文本添加到此字符串中。在我初学 C# 语言的时候,我也是这么理解的。
实际上却并非如此,而是创建一个新的字符串实例,给他分配足够的内存,然后存储合并的所有文本。
把第一行代码中的文本:"XXXXXX" 复制到新字符串中,再加上第二行代码中的文本:" is a gentleman."。
然后更新存储在 name 变量中的内存地址,使变量正确的指向新的字符串对象。旧的字符串对象被销毁了引用,并等待系统回收。
这样的方式本身并没有问题,但是如果需要频繁的进行字符串的操作的话,那就存在极大地性能问题。
因此,为了解决这一问题,微软推出了 System.Text.StringBuilder 类。在 StringBuilder 类中,仅限于替换、添加和删除字符串中文本的操作,但它的效率远远高于 String。
StringBuilder stringBuilder = new StringBuilder(30,300);
StringBuilder 类在初始化的时候,提供许多构造函数用来初始化当前实例的初始大小和可存储的最大字符数以及用来初始化当前实例的字符串。
实际上,当我们创建 StringBuilder 对象的时候,.NET 运行库会为当前的对象在内存中分配一块缓存区域,用以对字符串操作的预留空间。
在使用 StringBuilder 类的时候,最好将容量设置为字符串可能的最大长度,确保 StringBuilder 不需要重复分配内存。
如果字符的容量超过设置的最大容量,.NET 运行库将自动分配内存并翻倍。
对于我们 .NET 程序员而言,StringBuilder 与 String 的不同之处就在于,StringBuilder 可以显示的设置分配内存的大小,
而 String 只能根据你初始化时的字符串的大小由系统分配足够的内存。
所以,当要对字符串进行频繁的操作的时候,在 String 和 StringBuilder 之间,我们应该选择 StringBuilder。