一道面试题关于String引发的思考

ContractedBlock.gif ExpandedBlockStart.gif 示例代码
 
   
1 public static void Main()
2 {
3 string strA = " abcdef " ;
4 string strB = " abcdef " ;
5 Console.WriteLine(ReferenceEquals(strA, strB));
6 string strC = " abc " ;
7 string strD = strC + " def " ;
8 Console.WriteLine(ReferenceEquals(strA, strD));
9 strD = String.Intern(strD);
10 Console.WriteLine(ReferenceEquals(strA, strD));
11 }

猜一下输出结果,我做的时候猜错2个。应为不太理解内在原理。

  上面示例, 会给我们三个意外,也是关于执行结果的意外:首先,strA和strB为两个不同的String对象,按照一般的分析两次创建的不同对象,CLR将为其在托管堆分配不同的内存块,而ReferenceEquals方法用于判断两个引用是否指向同一对象实例,从结果来看strA和strB显然指向了同一内存地址; 

   其次,strD和strA在内容上也是一样的,然而其ReferenceEquals方法返回的结果为False,显然strA和strD并没有指向相同的内存块;最后,以静态方法Intern操作strD后,二者又指向了相同的对象,ReferenceEquals方法又返回True。

要想解释以上疑惑,只有请字符串驻留(String Interning)登场了。下面我们通过对字符串驻留技术的分析,来一步一步解开上述示例的种种疑惑。

  String类型区别于其他类型的最大特点是其恒定性。对字符串的任何操作,包括字符串比较,字符串链接,字符串格式化等会创建新的字符串,从而伴随着性能与内存的双重损耗。而String类型本身又是.NET中使用最频繁、应用最广泛的基本类型,因此CLR有必要有针对性的对其性能问题,采取特殊的解决办法。 事实上,CLR以字符串驻留机制来解决这一问题:对于相同的字符串,CLR不会为其分别分配内存空间,而是共享同一内存。因此,有两个问题显得尤为重要:

 l 一方面,CLR必须提供特殊的处理结构,来维护对相同字符串共享内存的机制。

 l 另一方面,CLR必须通过查找来添加新构造的字符串对象到其特定结构中。

 的确如此,CLR内部维护了一个哈希表(Hash Table)来管理其创建的大部分string对象。

其中,Key为string本身,而Value为分配给对应的string的内存地址

我们一步一步分析上述示例的执行过程,然后才能从总体上对字符串驻留机制有所了解。 string strA = "abcdef"; CLR初始化时,会创建一个空哈希表,当JIT编译方法时,会首先在哈希表中查找每一个字符串常量,显然第一次它不会找到任何“abcdef”常量,因此会在托管堆中创建一个新的string对象strA,并在哈希表中创建一个Key-Value对,将“abcdef”串赋给Key,而将strA对象的引用赋给Value,也就是说Value内保持了指向“abcdef”字符串在托管堆中的引用地址。这样就完成了第一次字符串的创建过程。 string strB = "abcdef"; 程序接着运行,JIT根据“abcdef”在哈希表中逐个查找,结果找到了该字符串,所以JIT不会执行任何操作,只是把找到的Key-Value对的Value值赋给strB对象。

由此可知,strA和strB具有相同的内存引用,所以ReferenceEquals方法当然返回true。

string strC = "abc"; string strD = strC + "def"; 接着,JIT以类似的过程来向哈希表中添加了“abc”字符串,并将引用返回给strC对象;但是strD对象的创建过程又有所区别,因为strD是动态生成的字符串,这样的字符串是不会被添加到哈希表中维护的,因此以ReferenceEquals来比较strA和strD会返回false。对于动态生成的字符串,因为没有添加到CLR内部维护的哈希表而使字符串驻留机制失效。但是,当我们需要高效的比较两个字符串是否相等时,可以手工启用字符串驻留机制,这就是调用String类型的两个静态方法,它们是: public static string Intern(string str); public static string IsInterned(string str); 二者的处理机制都是在哈希表中查找是否存在str参数字符串,如果找到就返回已存在的String对象的引用,否则Intern方法将该str字符串添加到哈希表中,并返回引用;而IsInterned方法则不会向哈希表中添加字符串,而只是返回null。

例如, strD = String.Intern(strD); Console.WriteLine(ReferenceEquals(strA, strD)); 我们就很容易解释上述代码的执行结果了。

ContractedBlock.gif ExpandedBlockStart.gif 看看这个输出什么?
 
   
1 public static void Main()
2 {
3 string strA = " abcdef " ;
4 string strC = " abc " ;
5 string strD = strC + " def " ;
6 Console.WriteLine(ReferenceEquals(strA, strD));
7 string strE = " abc " + " def " ;
8 Console.WriteLine(ReferenceEquals(strA, strE));
9 }

 动态生成字符串时,CLR调用了System::Concat来执行字符串链接;而直接赋值strE = “abc” + “def”的操作,编译器会自动将其连接为一个文本常量加载,因此会添加到内部哈希表中,这也是为什么最后strA和strE指向同一对象的原因了。

  最后,需要特别指出的是:字符串驻留是进程级的,可以跨应用程序域(AppDomain)而存在。垃圾回收不能释放哈希表中引用的字符串对象,只有进程结束这些对象才会被释放。因此,String类型的特殊性还表现在同一个字符串对象可以在不同的应用程序域中被访问,从而突破了AppDomain的隔离机制,其原因还是源于字符串的恒定性,因为是不可变的,所以根本没有必要再隔离。

转载于:https://www.cnblogs.com/gaodao/archive/2011/05/05/string.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值