原来是这样:C#中字符串的内存分配与驻留池

原来是这样:C#中字符串的内存分配与驻留池

刚开始学习C#的时候,就听说CLR对于String类有一种特别的内存管理机制:有时候,明明声明了两个String类的对象,但是他们偏偏却指向同一个实例。如下:

    
    
String s1 = " Hello " ; String s2 = " Hello " ; // s2和s1的实际值都是“Hello” bool same = ( object ) s1 == ( object ) s2; // 这里比较s1、s2是否引用了同一个对象实例 // 所以不能写作bool same = s1 == s2; // 因为String类重载了==操作符来比较String对象包含的实际值

这里的same会被赋值为true。也就是说s1真的和s2引用了同一个String对象。当然,应该注意到的是s1和s2都被统一赋值为同一个字符串“Hello”,这才是出现上述情况的原因。

现在我们初步得出结论,当有多个字符串变量包含了同样的字符串实际值时,CLR可能不会为它们重复地分配内存,而是让它们统统指向同一个字符串对象实例。(这里我说了“可能”,是因为某些情况下,确实也会发生同一个字符串实际值在内存中有多份副本同时存在。请继续往下看。)

我们知道,String类有很多特别的地方,其中之一就是它是“不会改变的”(immutable)。这说明在我们每次对一个String对象进行操作时(比如说使用Trim,Replace等方法),并不是真的对这个String对象的实例进行修改,而是返回一个新的String对象实例作为操作执行的结果。String对象的实例一经生成,到死都不会被改变了!

基于String类这样的特性,CLR让表示相同的字符串实际值的变量指向同一个String事例,就是完全合理的了。因为利用任何一个对String实例的引用所进行的修改操作都不会切实地影响到该实例的状态,也就不会影响到其他所有指向该实例的引用所表示的字符串实际值。CLR如此管理String类的内存分配,可以优化内存的使用情况,避免内存中包含冗余的数据。

为了实现这个机制,CLR默默地维护了一个叫做驻留池(Intern Pool)的表。这个表记录了所有在代码中使用字面量声明的字符串实例的引用。这说明使用字面量声明的字符串会进入驻留池,而其他方式声明的字符串并不会进入,也就不会自动享受到CLR防止字符串冗余的机制的好处了。这就是我上文提到的“某些情况下,确实也会发生同一个字符串实际值在内存中有多份副本同时存在”的例子。请看这个例子:

    
    
StringBuilder sb = new StringBuilder(); sb.Append( " He " ).Append( " llo " );
string s1 = " Hello " ; string s2 = sb.ToString(); bool same = ( object ) s1 == ( object ) s2;

这时same就不是true了,因为虽然s1,s2表示的是相同的字符串,但是由于s2不是通过字面量声明的,CLR在为sb.ToString()方法的返回值分配内存时,并不会到驻留池中去检查是否有值为“Hello”的字符串已经存在了,所以自然不会让s2指向驻留池内的对象。

为了让编程者能够强制CLR检查驻留池,以避免冗余的字符串副本,String类的设计者提供了一个名为Intern的类方法。下面是该方法的一个示例:

 

    
    
StringBuilder sb = new StringBuilder(); sb.Append( " He " ).Append( " llo " );
string s1 = " Hello " ; string s2 = String.Intern(sb.ToString()); bool same = ( object ) s1 == ( object ) s2;

好了,same又是true了。Intern方法接受一个字符串作为参数,它会在驻留池中检查是否存在参数所表示的字符串。如果存在,则返回那个驻留池中的字符串的引用;否则向驻留池中加入一个新的表示相同值的字符串,并返回这个字符串的引用。不过要注意的是,就算Intern方法在驻留池中找到了相同值的字符串,也不能让您省却一次字符串内存分配的操作,因为作为参数的字符串已经被分配了一次内存了。而使用Intern方法的好处在于,如果Intern方法在驻留池中找到了相同值的字符串,此时虽然在内存中存在两份该字符串的副本(一份是参数,一份是驻留池中的),但是随着时间的流逝,参数所引用的那个副本会被垃圾回收掉,这样对于该字符串内存中就不存在冗余了。

 

 

 

当您的程序中存在某个方法,可以根据不同的上下文环境创建并返回一个很长的字符串,而在程序运行的过程中它有会经常返回同样的字符串时,您可能就要考虑考虑使用Intern方法来提高内存的利用率了。

 

 

 

不过同样值得注意的是,使用Intern方法让一个字符串存活于驻留池中也有一个副作用:即使已经不存在任何其它引用指向驻留池中的字符串了,这个字符串仍然不一定会被垃圾回收掉。也就是说即使驻留池中的字符串已经没有用处了,它可能也要等到CLR终结时才被销毁。当您使用Intern方法的时候,也应该考虑到这个特殊的行为。

### C#字符串参数传递特性 在 C# 中,`string` 类型被定义为引用类型。然而,在实际操作中 `string` 的行为有些特殊之处。 当涉及到参数传递时,尽管 `string` 是引用类型,但在默认情况下它表现得像值类型一样进行复制传递[^3]。这意味着当你把一个字符串变量作为参数传递给函数时,实际上是在传递该字符串的一个副本而不是原始对象的引用。这种机制确保了调用者无法通过修改接收方的方法内部所使用的局部拷贝来改变原字符串的内容。 但是需要注意的是,由于 .NET 运行库实现了字符串驻留(interning),对于相同的字面量字符串来说,它们确实会在内存中共存于同一位置并共享相同的数据结构[^1]。因此如果两个不同的地方创建了具有完全相等内容的文字字符串,则这两个变量将会指向堆中的同一个实例;但这并不影响上述关于参数传递过程中发生的隐式复制过程的事实。 下面是一个简单的例子用于展示这一点: ```csharp using System; class StringPassExample { public static void ChangeString(string inputStr) { inputStr += " changed"; Console.WriteLine($"Inside method: {inputStr}"); } public static void Main() { string originalStr = "original"; Console.WriteLine($"Before calling method: {originalStr}"); ChangeString(originalStr); Console.WriteLine($"After calling method: {originalStr}"); } } ``` 在这个程序里,即使方法内改变了传入的字符串,外部原来的字符串仍然保持不变,这证明了虽然 `string` 是引用类型,但它按照值的方式进行了传递。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值