C#的字符串优化-String.Intern、IsInterned

首先看一段程序:

using System;
 
class Program
{
 static void Main(string[] args)
 {
  string a = "hello world";
  string b = a;
  a = "hello";
  Console.WriteLine("{0}, {1}", a, b);
  Console.WriteLine(a == b);
  Console.WriteLine(object.ReferenceEquals(a, b));
 }
}

这个没有什么特殊的地方,相信大家都知道运行结果:

hello, hello world
False
False

第二个WriteLine使用==比较两个字符串,返回False是因为他们不一致。而最后一个WriteLine返回False,因为a、b的引用不一致。
接下来,我们在代码的最后添加代码:

Console.WriteLine((a + " world") == b);
Console.WriteLine(object.ReferenceEquals((a + " world"), b));

这个的输出,相信也不会出乎大家的意料。前者返回True,因为==两边的内容相等;后者为False,因为+运算符执行完毕后,会创建一个新的string实例,这个实例与b的引用不一致。
上面这些就是对象的通常工作方式,两个独立的对象可以拥有同样的内容,但他们却是不同的个体。

接下来,我们就来说一下string不寻常的地方

看一下下面这段代码:

using System;

class Program
{
 static void Main(string[] args)
 {
  string hello = "hello";
  string helloWorld = "hello world";
  string helloWorld2 = hello + " world";
   
  Console.WriteLine("{0}, {1}: {2}, {3}", helloWorld, helloWorld2,
  helloWorld == helloWorld2,
  object.ReferenceEquals(helloWorld, helloWorld2));
  }
}

运行一下,结果为:

hello world, hello world: True, False

再一次,没什么意外,==返回true因为他们内容相同,ReferenceEquals返回False因为他们是不同的引用。
现在在后面添加这样的代码:

helloWorld2 = "hello world";
Console.WriteLine("{0}, {1}: {2}, {3}", helloWorld, helloWorld2,
    helloWorld == helloWorld2,
    object.ReferenceEquals(helloWorld, helloWorld2));

运行,结果为:

hello world, hello world: True, True

等一下,这里的hellowWorld与helloWorld2引用一致?这个结果,相信很多人都有些接受不了。这里的helloWorld2与上面的hello + " world"应该是一样的,但为什么ReferenceEquals返回的是True?

String.Intern


有经验的程序员们,应该知道,一个大型项目中,字符串的数量是巨大的。有些时候会出现几百、几千、甚至几万的重复字符串存在。这些字符串的内容相同,但却会重复分配内存,占用巨额的存储空间,这个肯定是要优化处理的。而C#在处理这个问题的时候,采用的就是普遍的做法,建立内部的池,池中每一个不同的字符串存在唯一一个个体在池中(这个方案在各种大型项目中都能见得到)。而C#毕竟是一种语言,而不是一个面向某个具体领域的技术,所以,它不能将这种内部的池技术,做成全部自动化的。因为我们不知道,将来C#会被使用到何种规模的项目中。如果完全自动化维护这个内部池,可能会在大型项目中,造成内存的巨大浪费,毕竟不是所有的字符串都有必要加到这个常驻的池中的。于是,C#提供了String.Intern和String.IsInterned接口,交给程序员自己维护内部的池。
String.Intern的工作方式很好理解,你将一个字符串作为参数使用这个接口,如果这个字符串已经存在池中,就返回这个存在的引用;如果不存在就将它加入到池中,并返回引用,例如:

Console.WriteLine(object.ReferenceEquals(String.Intern(helloWorld), String.Intern(helloWorld2)));

这段代码将返回True,尽管helloWorld与helloWorld2的引用不同,但他们的内容相同。
这里我们花几分钟,测试一下String.Intern,因为在某些情况下,它产生的结果,有点违反直觉。这里是一个例子:

string a = new string(new char[] {'a', 'b', 'c'});
object o = String.Copy(a);
Console.WriteLine(object.ReferenceEquals(o, a));
String.Intern(o.ToString());
Console.WriteLine(object.ReferenceEquals(o, String.Intern(a)));

第一个WriteLine返回False很好理解,因为String.Copy创建了一个a的新的实例,所以,o与a的引用不用。
但为什么第二个WriteLine返回的是True?思考一下吧,下面再看一个例子:

object o2 = String.Copy(a);
String.Intern(o2.ToString());
Console.WriteLine(object.ReferenceEquals(o2, String.Intern(a)));

这个看起来,与上面的做了同样的事,但为什么WriteLine返回的是False?

首先,需要说明一下ToString的工作方式,它总是返回它自身的引用。o是一个指向“abc”的变量,调用ToString返回的就是这个引用。所以,对于上面的内容,可以这样解释:

  1. 开始,变量a指向字符串对象“abc”(#1),变量o指向另一个字符串对象(#2),也包含“abc”。
  2. 调用String.Intern(o.ToString())将对象#2的引用添加到内部池中。
  3. 现在#2对象已经存在池中了,任何时候,使用“abc”调用String.Intern都将返回#2的引用(o指向了这个对象)。
  4. 所以,当你使用ReferenceEquals比较o和String.Intern(a)时,返回True。因为String.Intern(a)返回的是#2的引用。
  5. 现在我们创建一个新的变量o2,使用String.Copy(a)创建一个新的对象#3,它也包含“abc”。
  6. 调用String.Intern(o2.ToString())没有向内部池中添加任何内容,因为“abc”已经存在(#2)。
  7. 所以,此时调用Intern返回的是对象#2的引用。注意,这里并没有使用类似o2 = String.Intern(o2.ToString())这样的代码。
  8. 这就是为什么最后一行WriteLine打印的False的原因,因为我们在尝试比较#3与#2的引用。如果如7中所说,添加o2 = String.Intern(o2.ToString())这样的代码,WriteLine返回的就是True。

String.IsInterned


IsInterned,正如它的名字,判断一个字符串是不是已经在内部池中。如果传入的字符串已经在池中,则返回这个字符串对象的引用,如果不再池中,返回null。
下面是一个IsInterned例子:

string s = new string(new char[] {'x', 'y', 'z'});
Console.WriteLine(String.IsInterned(s) ?? "not interned");
String.Intern(s);
Console.WriteLine(String.IsInterned(s) ?? "not interned");
Console.WriteLine(object.ReferenceEquals(
String.IsInterned(new string(new char[] { 'x', 'y', 'z' })), s));

第一个WriteLine打印的是“not interned”,因为“xyz”还没有存在于内部池中;第二个WriteLine打印了“xyz”因为现在内部池中有了“xyz”;第三个WriteLine打印True,因为对象引用的就是内部池中的“xyz”。

常量字符串自动被加入内部池

改变最后一行代码为:

Console.WriteLine(object.ReferenceEquals("xyz", s));

你会发现,奇怪的事情发生了,这些代码不再输出“not interned”了,并且最后的两个WriteLine输出的是False!发生了什么?
原因就是这个最后添加的那行代码中的常量“xyz”,CLR会将程序中使用的字符常量自动添加到内部池中。所以,当最后一行被添加之后,“xyz”在程序“运行之前”(避免严谨,这里用引号)就已经存在于内部池中。所以,当调用String.IsInterned的时候,返回的不再是null,而是指向“xyz”的引用。这也解释了,为什么后面的ReferenceEquals返回False,因为s从来没有被加到内部池中,其指向也不是内部池的"xyz"。

编译器比你想象的要聪明

改变最后一行代码为:

Console.WriteLine(object.ReferenceEquals("x" + "y" + "z", s));

运行一下,你会发现运行结果和直接使用“xyz”一样。但这里使用了+运算符啊?编译器不应该知道”x“+"y"+"z"最终的结果吧?
实际上,如果你将”x“+"y"+"z"替换为String.Format("{0}{1}{2}",'x','y','z'),结果确实就不一样了。某种原因,CLR会将使用+运算符链接的字符串视为常量,而String.Format却需要在运行时才能知道结果。为什么?看一下下面的代码:

using System;
  
class Program {
 public static void Main() {
  Console.WriteLine("x" + "y" + "z");
  }
}

这段代码编译之后,使用Ildasm.exe查看,会看到:


Screenshot - ILDasm intern-xyz Main method.png

看到了吧,编译器足够聪明,将”x“+"y"+"z"替换为”xyz“。

本文大部分内容来自:http://broadcast.oreilly.com/2010/08/understanding-c-stringintern-m.html,翻译、批注:小匠头

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,559评论 18 399
  • 缘起 开始介绍 intern()方法前,先看一个简单的 Java程序吧!下面是一段 Java代码,代码内容比较简单...
    LilacZiyun阅读 2,714评论 6 17
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,580评论 18 139
  • 前面我们总结了数组操作,这里我们将总结字符串相关的知识,除了总结String的API用法,同时我们还会总结一些相关...
    HCherisher阅读 3,598评论 2 6
  • 从小我就是大人嘴里说的“爱打扮”的孩子,用母亲调侃的话说就是,“当初把你和你姐姐生错了,那么爱打扮要是做个女生该多...
    Ray先森爱健身阅读 695评论 10 6