C#的字符串優(yōu)化-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));
 }
}

這個沒有什么特殊的地方鹏溯,相信大家都知道運行結(jié)果:

hello, hello world
False
False

第二個WriteLine使用==比較兩個字符串捏萍,返回False是因為他們不一致太抓。而最后一個WriteLine返回False,因為a令杈、b的引用不一致腻异。
接下來,我們在代碼的最后添加代碼:

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

這個的輸出这揣,相信也不會出乎大家的意料悔常。前者返回True,因為==兩邊的內(nèi)容相等给赞;后者為False机打,因為+運算符執(zhí)行完畢后,會創(chuàng)建一個新的string實例片迅,這個實例與b的引用不一致残邀。
上面這些就是對象的通常工作方式,兩個獨立的對象可以擁有同樣的內(nèi)容柑蛇,但他們卻是不同的個體芥挣。

接下來,我們就來說一下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));
  }
}

運行一下耻台,結(jié)果為:

hello world, hello world: True, False

再一次空免,沒什么意外,==返回true因為他們內(nèi)容相同盆耽,ReferenceEquals返回False因為他們是不同的引用蹋砚。
現(xiàn)在在后面添加這樣的代碼:

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

運行,結(jié)果為:

hello world, hello world: True, True

等一下摄杂,這里的hellowWorld與helloWorld2引用一致坝咐?這個結(jié)果,相信很多人都有些接受不了析恢。這里的helloWorld2與上面的hello + " world"應(yīng)該是一樣的墨坚,但為什么ReferenceEquals返回的是True?

String.Intern


有經(jīng)驗的程序員們映挂,應(yīng)該知道泽篮,一個大型項目中,字符串的數(shù)量是巨大的袖肥。有些時候會出現(xiàn)幾百咪辱、幾千振劳、甚至幾萬的重復(fù)字符串存在椎组。這些字符串的內(nèi)容相同,但卻會重復(fù)分配內(nèi)存历恐,占用巨額的存儲空間寸癌,這個肯定是要優(yōu)化處理的专筷。而C#在處理這個問題的時候,采用的就是普遍的做法蒸苇,建立內(nèi)部的池磷蛹,池中每一個不同的字符串存在唯一一個個體在池中(這個方案在各種大型項目中都能見得到)。而C#畢竟是一種語言溪烤,而不是一個面向某個具體領(lǐng)域的技術(shù)味咳,所以,它不能將這種內(nèi)部的池技術(shù)檬嘀,做成全部自動化的槽驶。因為我們不知道,將來C#會被使用到何種規(guī)模的項目中鸳兽。如果完全自動化維護這個內(nèi)部池掂铐,可能會在大型項目中,造成內(nèi)存的巨大浪費揍异,畢竟不是所有的字符串都有必要加到這個常駐的池中的全陨。于是,C#提供了String.Intern和String.IsInterned接口衷掷,交給程序員自己維護內(nèi)部的池辱姨。
String.Intern的工作方式很好理解,你將一個字符串作為參數(shù)使用這個接口戚嗅,如果這個字符串已經(jīng)存在池中炮叶,就返回這個存在的引用;如果不存在就將它加入到池中渡处,并返回引用镜悉,例如:

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

這段代碼將返回True,盡管helloWorld與helloWorld2的引用不同医瘫,但他們的內(nèi)容相同侣肄。
這里我們花幾分鐘,測試一下String.Intern醇份,因為在某些情況下稼锅,它產(chǎn)生的結(jié)果,有點違反直覺僚纷。這里是一個例子:

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創(chuàng)建了一個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”的變量潮剪,調(diào)用ToString返回的就是這個引用涣楷。所以,對于上面的內(nèi)容抗碰,可以這樣解釋:

  1. 開始总棵,變量a指向字符串對象“abc”(#1),變量o指向另一個字符串對象(#2)改含,也包含“abc”情龄。
  2. 調(diào)用String.Intern(o.ToString())將對象#2的引用添加到內(nèi)部池中。
  3. 現(xiàn)在#2對象已經(jīng)存在池中了捍壤,任何時候骤视,使用“abc”調(diào)用String.Intern都將返回#2的引用(o指向了這個對象)。
  4. 所以鹃觉,當(dāng)你使用ReferenceEquals比較o和String.Intern(a)時专酗,返回True。因為String.Intern(a)返回的是#2的引用盗扇。
  5. 現(xiàn)在我們創(chuàng)建一個新的變量o2祷肯,使用String.Copy(a)創(chuàng)建一個新的對象#3,它也包含“abc”疗隶。
  6. 調(diào)用String.Intern(o2.ToString())沒有向內(nèi)部池中添加任何內(nèi)容佑笋,因為“abc”已經(jīng)存在(#2)。
  7. 所以斑鼻,此時調(diào)用Intern返回的是對象#2的引用蒋纬。注意,這里并沒有使用類似o2 = String.Intern(o2.ToString())這樣的代碼坚弱。
  8. 這就是為什么最后一行WriteLine打印的False的原因蜀备,因為我們在嘗試比較#3與#2的引用。如果如7中所說荒叶,添加o2 = String.Intern(o2.ToString())這樣的代碼碾阁,WriteLine返回的就是True。

String.IsInterned


IsInterned些楣,正如它的名字脂凶,判斷一個字符串是不是已經(jīng)在內(nèi)部池中宪睹。如果傳入的字符串已經(jīng)在池中,則返回這個字符串對象的引用艰猬,如果不再池中,返回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”還沒有存在于內(nèi)部池中;第二個WriteLine打印了“xyz”因為現(xiàn)在內(nèi)部池中有了“xyz”道宅;第三個WriteLine打印True食听,因為對象引用的就是內(nèi)部池中的“xyz”。

常量字符串自動被加入內(nèi)部池

改變最后一行代碼為:

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

你會發(fā)現(xiàn)污茵,奇怪的事情發(fā)生了樱报,這些代碼不再輸出“not interned”了,并且最后的兩個WriteLine輸出的是False泞当!發(fā)生了什么迹蛤?
原因就是這個最后添加的那行代碼中的常量“xyz”,CLR會將程序中使用的字符常量自動添加到內(nèi)部池中襟士。所以盗飒,當(dāng)最后一行被添加之后,“xyz”在程序“運行之前”(避免嚴(yán)謹(jǐn)陋桂,這里用引號)就已經(jīng)存在于內(nèi)部池中逆趣。所以,當(dāng)調(diào)用String.IsInterned的時候嗜历,返回的不再是null宣渗,而是指向“xyz”的引用。這也解釋了梨州,為什么后面的ReferenceEquals返回False痕囱,因為s從來沒有被加到內(nèi)部池中,其指向也不是內(nèi)部池的"xyz"暴匠。

編譯器比你想象的要聰明

改變最后一行代碼為:

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

運行一下咐蝇,你會發(fā)現(xiàn)運行結(jié)果和直接使用“xyz”一樣。但這里使用了+運算符跋锊椤有序?編譯器不應(yīng)該知道”x“+"y"+"z"最終的結(jié)果吧?
實際上岛请,如果你將”x“+"y"+"z"替換為String.Format("{0}{1}{2}",'x','y','z')旭寿,結(jié)果確實就不一樣了。某種原因崇败,CLR會將使用+運算符鏈接的字符串視為常量盅称,而String.Format卻需要在運行時才能知道結(jié)果肩祥。為什么?看一下下面的代碼:

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“将饺。

本文大部分內(nèi)容來自:http://broadcast.oreilly.com/2010/08/understanding-c-stringintern-m.html,翻譯痛黎、批注:小匠頭

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末予弧,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子湖饱,更是在濱河造成了極大的恐慌掖蛤,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,036評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件井厌,死亡現(xiàn)場離奇詭異蚓庭,居然都是意外死亡,警方通過查閱死者的電腦和手機仅仆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,046評論 3 395
  • 文/潘曉璐 我一進店門彪置,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蝇恶,你說我怎么就攤上這事拳魁。” “怎么了撮弧?”我有些...
    開封第一講書人閱讀 164,411評論 0 354
  • 文/不壞的土叔 我叫張陵潘懊,是天一觀的道長。 經(jīng)常有香客問我贿衍,道長授舟,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,622評論 1 293
  • 正文 為了忘掉前任贸辈,我火速辦了婚禮释树,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘擎淤。我一直安慰自己奢啥,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,661評論 6 392
  • 文/花漫 我一把揭開白布嘴拢。 她就那樣靜靜地躺著桩盲,像睡著了一般。 火紅的嫁衣襯著肌膚如雪席吴。 梳的紋絲不亂的頭發(fā)上赌结,一...
    開封第一講書人閱讀 51,521評論 1 304
  • 那天捞蛋,我揣著相機與錄音,去河邊找鬼柬姚。 笑死拟杉,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的量承。 我是一名探鬼主播搬设,決...
    沈念sama閱讀 40,288評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼宴合!你這毒婦竟也來了焕梅?” 一聲冷哼從身側(cè)響起迹鹅,我...
    開封第一講書人閱讀 39,200評論 0 276
  • 序言:老撾萬榮一對情侶失蹤卦洽,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后斜棚,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體阀蒂,經(jīng)...
    沈念sama閱讀 45,644評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,837評論 3 336
  • 正文 我和宋清朗相戀三年弟蚀,在試婚紗的時候發(fā)現(xiàn)自己被綠了蚤霞。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,953評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡义钉,死狀恐怖昧绣,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情捶闸,我是刑警寧澤夜畴,帶...
    沈念sama閱讀 35,673評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站删壮,受9級特大地震影響贪绘,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜央碟,卻給世界環(huán)境...
    茶點故事閱讀 41,281評論 3 329
  • 文/蒙蒙 一税灌、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧亿虽,春花似錦菱涤、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,889評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至坯认,卻和暖如春翻擒,著一層夾襖步出監(jiān)牢的瞬間氓涣,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,011評論 1 269
  • 我被黑心中介騙來泰國打工陋气, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留劳吠,地道東北人。 一個月前我還...
    沈念sama閱讀 48,119評論 3 370
  • 正文 我出身青樓巩趁,卻偏偏與公主長得像痒玩,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子议慰,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,901評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法蠢古,類相關(guān)的語法,內(nèi)部類的語法别凹,繼承相關(guān)的語法草讶,異常的語法,線程的語...
    子非魚_t_閱讀 31,631評論 18 399
  • 緣起 開始介紹 intern()方法前炉菲,先看一個簡單的 Java程序吧堕战!下面是一段 Java代碼,代碼內(nèi)容比較簡單...
    LilacZiyun閱讀 2,733評論 6 17
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理拍霜,服務(wù)發(fā)現(xiàn)嘱丢,斷路器,智...
    卡卡羅2017閱讀 134,657評論 18 139
  • 前面我們總結(jié)了數(shù)組操作祠饺,這里我們將總結(jié)字符串相關(guān)的知識越驻,除了總結(jié)String的API用法,同時我們還會總結(jié)一些相關(guān)...
    HCherisher閱讀 3,621評論 2 6
  • 從小我就是大人嘴里說的“愛打扮”的孩子道偷,用母親調(diào)侃的話說就是缀旁,“當(dāng)初把你和你姐姐生錯了,那么愛打扮要是做個女生該多...
    Ray先森愛健身閱讀 704評論 10 6