C# String字符串案例詳解
string是一種很特殊的數據類型,它既是基元類型又是引用類型,在編譯以及運行時,.Net都對它做瞭一些優化工作,正式這些優化工作有時會迷惑編程人員,使string看起來難以琢磨。這篇文章共四節,來講講關於string的陌生一面。
一.恒定的字符串
要想比較全面的瞭解stirng類型,首先要清楚.Net中的值類型與引用類型。
在C#中,以下數據類型為值類型: bool、byte、char、enum、sbyte以及數字類型(包括可空類型)
以下數據類型為引用類型: class、interface、delegate、object、stirng
看到瞭嗎,我們要討論的stirng赫然其中。被聲明為string型變量存放於堆中,是一個徹頭徹尾的引用類型。那麼許多同學就會對如下代碼產生有疑問瞭,難道string類型也會“牽一發而動全身”嗎?讓我們先來看看以下三行代碼有何玄機:
string a = "str_1"; string b = a; a = "str_2";
不要說無聊,這一點時必須講清楚的!在以上代碼中,第3行的“=”有一個隱藏的秘密:它的作用我們可以理解為新建,而不是對變量“a”的修改。以下是IL代碼,可以說明這一點:
.maxstack 1 .locals init ([0] string a,[1] string b) IL_0000: nop IL_0001: ldstr "str_1" IL_0006: stloc.0 IL_0007: ldloc.0 IL_0008: stloc.1 IL_0009: ldstr "str_2" IL_000e: stloc.0 //以上2行對應 C#代碼 a = "str_2"; IL_0015: ret
可以看出IL代碼的第1、6行,由ldstr指令創建字符串”str_1″,並將其關聯到瞭變量“a”中;7、8行直接將堆棧頂部的值彈出並關聯到變量“b”中;9、10由ldstr創建字符串”str_2″,關聯在變量“a”中(並沒有像我們想象的那樣去修改變量a的舊值,而是產生瞭新的字符串);
在C#中,如果用new關鍵字實例化一個類,對應是由IL指令newobj來完成的;而創建一個字符串,則由ldstr指令完成,看到ldstr指令,我們即可認為,IL希望創建一個新的字符串 。(註意:是IL希望創建一個字符串,而最終是否創建,還要在運行時由字符串的駐留機制決定,這一點下面的章節會有介紹。)
所以,第三行C#代碼(a = “str_2”;)的樣子看起來是在修改變量a的舊值”str_1″,但實際上是創建瞭一個新的字符串”str_2″,然後將變量a的指針指向瞭”str_2″的內存地址,而”str_1″依然在內存中沒有受到任何影響,所以變量b的值沒有任何改變—這就是string的恒定性,同學們,一定要牢記這一點,在.Net中,string類型的對象一旦創建即不可修改!包括ToUpper、SubString、Trim等操作都會在內存中產生新的字符串。
本節重點回顧:由於stirng類型的恒定性,讓同學友們經常誤解,string雖屬引用類型但經常表現出值的特性,這是由於不瞭解string的恒定性造成的,根本不是“值的特性”。例如:
string a = "str_1"; a = "str_2";
這樣會在內存中創建”str_1″和”str_2″兩個字符串,但隻有”str_2″在被使用,”str_1″不會被修改或消失,這樣就浪費瞭內存資源,這也是為什麼在做大量字符串操作時,推薦使用StringBuilder的原因。
二..Net中字符串的駐留(重要)
在第一節中,我們講瞭字符串的恒定性,該特性又為我們引出瞭字符串的另一個重要特性:字符串駐留。
從某些方面講,正是字符串的恒定性,才造就瞭字符串的駐留機制,也為字符串的線程同步工作大開方便之門(同一個字符串對象可以在不同的應用程序域中被訪問,所以駐留的字符串是進程級的,垃圾回收不能釋放這些字符串對象,隻有進程結束這些對象才被釋放)。
我們用以下2行代碼來說明字符串的駐留現象:
string a = "str_1"; string b = "str_1";
請各位同學友思考一下,這2行代碼會在內存中產生瞭幾個string對象?你可能會認為產生2個:由於聲明瞭2個變量,程序第1行會在內存中產生”str_1″供變量a所引用;第2行會產生新的字符串”str_1″供變量b所引用,然而真的是這樣嗎?我們用ReferenceEquals這個方法來看一下變量a與b的內存引用地址:
string a = "str_1"; string b = "str_1"; Response.Write(ReferenceEquals(a,b)); //比較a與b是否來自同一內存引用 //輸出:True
哈,各位同學看到瞭嗎,我們用ReferenceEquals方法比較a與b,雖然我們聲明瞭2個變量,但它們竟然來自同一內存地址!這說明string b = “str_1”;根本沒有在內存中產生新的字符串。
這是因為,在.Net中處理字符串時,有一個很重要的機制,叫做字符串駐留機制。由於string是編程中用到的頻率較高的一種類型,CLR對相同的字符串,隻分配一次內存。CLR內部維護著一塊特殊的數據結構,我們叫它字符串池,可以把它理解成是一個HashTable,這個HashTable維護著程序中用到的一部分字符串,HashTable的Key是字符串的值,而Value則是字符串的內存地址。一般情況下,程序中如果創建一個string類型的變量,CLR會首先在HashTable遍歷具有相同Hash Code的字符串,如果找到,則直接把該字符串的地址返回給相應的變量,如果沒有才會在內存中新建一個字符串對象。
所以,這2行代碼隻在內存中產生瞭1個string對象,變量b與a共享瞭內存中的”str_1″。
好瞭,結合第一節所講到的字符串恒定性與第二節所講到的駐留機制,來理解一下下面3行代碼吧:
string a = "str_1"; //聲明變量a,將變量a的指針指向內存中新產生的"str_1"的地址 a = "str_2"; //CLR先會在字符串池中遍歷,查看"str_2"是否已存在,如果沒有,則新建"str_2",並修改變量a的指針,指向"str_2"內存地址,"str_1"保持不變。(字符串恒定) string c = "str_2"; //CLR先會在字符串池中遍歷"str_2"是否已存在,如果存在,則直接將變量c的指針指向"str_2"的地址。(字符串駐留)
那麼如果是動態創建字符串呢?字符串還會不會有駐留現象呢?
我們分3種情況講解動態創建字符串時,駐留機制的表現:
(1).字符串常量連接
string a = "str_1" + "str_2"; string b = "str_1str_2"; Response.Write(ReferenceEquals(a,b)); //比較a與b是否來自同一內存引用 //輸出 :True
IL代碼:
.maxstack 1 .locals init ([0] string a,[1] string b) IL_0000: nop IL_0001: ldstr “str_1str_2” IL_0006: stloc.0 IL_0007: ldstr “str_1str_2” IL_000c: stloc.1 IL_000d: ret
其中第1、6行對應c#代碼string a = “str_1” + “str_2”;第7、8對應c# string b = “str_1str_2”;可以看出,字符串常量連接時,程序在被編譯為IL代碼前,編譯器已經計算出瞭字符串常量連接的結果,ldstr指令直接處理編譯器計算後的字符串值,所以這種情況字符串駐留機制有效!
(2).字符串變量連接
string a = "str_1"; string b = a + "str_2"; string c = "str_1str_2"; Response.Write(ReferenceEquals(b,c)); //輸出:False
IL代碼:
.maxstack 2 .locals init ([0] string a, [1] string b, [2] string c) IL_0000: nop IL_0001: ldstr “str_1” IL_0006: stloc.0 IL_0007: ldloc.0 IL_0008: ldstr “str_2” IL_000d: call string [mscorlib]System.String::Concat(string,string) IL_0012: stloc.1 IL_0013: ldstr “str_1str_2” IL_0018: stloc.2 IL_0019: ret
其中第1、6行對應string a = “str_1”;第7、8、9行對應string b = a + “str_2”;,IL用的是Concat方法連接字符串,第13、18行對應string c = “str_1str_2”;可以看出,字符串變量連接時,IL使用Concat方法,在運行時生成最終的連接結果,所以這種情況字符串駐留機制無效!
(3).顯式實例化
string a = "a"; string b = new string('a',1); Response.Write(ReferenceEquals(a, b)); //輸出 False
IL代碼:
.maxstack 3 .locals init ([0] string a,[1] string b) IL_0000: nop IL_0001: ldstr "a" IL_0006: stloc.0 IL_0007: ldc.i4.s 97 IL_0009: ldc.i4.1 IL_000a: newobj instance void [mscorlib]System.String::.ctor(char, int32) IL_000f: stloc.1 IL_0010: ret
這種情況比較好理解,IL使用newobj來實例化一個字符串對象,駐留機制無效。從string b = new string(‘a’,1);這行代碼我們可以看出,其實string類型實際上是由char[]實現的,一個string的誕生絕不像我們想想的那樣簡單,要由棧、堆同時配合,才會有一個string的誕生。這一點在第四節會有介紹。
當然,當字符串駐留機制無效時,我們可以很簡便的使用string.Intern方法將其手動駐留至字符串池中,例如以下代碼:
string a = "a"; string b = new string('a',1); Response.Write(ReferenceEquals(a, string.Intern(b))); //輸出:True (程序返回Ture,說明變量"a"與"b"來自同一內存地址。)
三.有趣的比較操作
在第一節與第二節中,我們分別介紹瞭字符串的恒定性與與駐留性,如果這位同學友覺得完全掌握瞭以上內容,那麼就在第三節中檢驗一下自己的學習成果吧!以下10段簡單的代碼將通過值比較與地址引用比較,來說明前兩節講到的內容,大傢也可以通過這些代碼來檢測一下自己對string的瞭解程度。
代碼一:
string a = "str_1"; string b = "str_1"; Response.Write(a.Equals(b)); Response.Write(ReferenceEquals(a,b)); //輸出:True (Equals比較字符串對象的值) //輸出:True (ReferenceEquals比較字符串對象的引用,由於字符串駐留機制,a與b的引用相同)
代碼二:
string a = "str_1str_2"; string b = "str_1"; string c = "str_2"; string d = b + c; Response.Write(a.Equals(d)); Response.Write(ReferenceEquals(a, d)); //輸出:True (Equals比較字符串對象的值) //輸出:False(ReferenceEquals比較字符串對象的引用,由於變量d的值為變量連接的結果,字符串駐留機制無效)
代碼三:
string a = "str_1str_2"; string b = "str_1" + "str_2"; Response.Write(a.Equals(b)); Response.Write(ReferenceEquals(a, b)); //輸出:True (Equals比較字符串對象的值) //輸出:True (ReferenceEquals比較字符串對象的引用,由於變量b的值為常量連接的結果,字符串駐留機制有效。如果變量b的值由“常量+變量”的方式得出,則字符串駐留無效)
代碼四:
string a = "str_1"; string b = String.Copy(a); Response.Write(a.Equals(b)); Response.Write(ReferenceEquals(a, b)); //輸出:True (Equals比較字符串對象的值) //輸出:False(ReferenceEquals比較字符串對象的引用,Copy操作產生瞭新的string對象)
代碼五:
string a = "str_1"; string b = String.Copy(a); b = String.Intern(b); Response.Write(a.Equals(b)); Response.Write(ReferenceEquals(a, b)); //輸出:True (Equals比較字符串對象的值) //輸出:True (ReferenceEquals比較字符串對象的引用,String.Intern實現瞭字符串駐留)
代碼六:
string a = "str_1"; string b = String.Copy(a); string c = "str_1"; Response.Write((object)a == (object)b); Response.Write((object)a == (object)c); //輸出:False(“==”在兩邊為引用類型時,則比較引用的地址,所以a與b為不同引用) //輸出:True (“==”在兩邊為引用類型時,則比較引用的地址,所以a與c的引用相同)(原文:ReferenceEquals比較字符串對象的引用,a與c由於字符串駐留機制,引用相同)
代碼七:
string a = "str_1"; string c = "str_1"; Response.Write(a == c); //輸出:True(剛才我們提到過,“==”在兩邊為引用類型時,則比較引用的地址;如果是值類型時則需要比較引用和值。string為引用類型,那麼上面的代碼是比較瞭變量a與c的地址還是地址和值呢? 答案是:比較瞭地址和值!因為在string類型比較的時候,“==”已經被重載為“Equals”瞭,所以,雖然你在用“==”比較兩個引用類型,但實際上是在用“Equals”比較它們的地址和值!(先比較地址,地址不等再比較值))
代碼八:
string a = "a"; string b = new string('a', 1); Response.Write(a.Equals(b)); Response.Write(ReferenceEquals(a, b)); //輸出:True (Equals比較值,a與b的值相同) //輸出:False(ReferenceEquals比較字符串對象的引用)
代碼九:
string a = "a"; string b = new string('a', 1); Response.Write(a.Equals(string.Intern(b))); Response.Write(ReferenceEquals(a, string.Intern(b))); //輸出:True (Equals比較值,無論是否Intern都會相同) //輸出:True (ReferenceEquals比較字符串對象的引用,Intern已經將b駐留至字符串池內)
代碼十:
string a = "str"; string b = "str_2".Substring(0,3); Response.Write(a.Equals(b)); Response.Write(ReferenceEquals(a, b)); //輸出:True (Equals比較值,a與c的值相同) //輸出:False(ReferenceEquals比較字符串對象的引用,Substring操作產生瞭新的字符串對象)
四.藝海拾貝
這一節將主要給大傢介紹一些string的常見問題。
(1)“string = ”與“new stirng()”的區別
string test = "a"; string test = new string('a', 1);
以上兩行代碼的效果是一樣的,它們的區別在於加載”a”的時間不同:第一行的“a”是一個常量,在編譯期就已經被放在一個叫做常量池的地方瞭,常量池通常裝載一些在編譯期被確定下來的數據,例如類、接口等等;而第二行是運行時CLR在堆中生成的值為“a”的字符串對象,所以後者沒有字符串駐留。
(2). string 與 String的區別
String的大名叫做System.String,在編譯為IL代碼時,string和System.String會生成完全相同的代碼:(ps:long和System.Int64,float和System.Single等也有此特性)
C#代碼:
string str_test = "test"; System.String Str_test = "test";
IL代碼:
// 代碼大小 14 (0xe) .maxstack 1 .locals init ([0] string str_test,[1] string Str_test) IL_0000: nop IL_0001: ldstr "test" IL_0006: stloc.0 IL_0007: ldstr "test" IL_000c: stloc.1 IL_000d: ret
所以,二者的區別並不在於底層,而是在於string是類似於int的基元類型;System. String是框架類庫(FCL)的基本類型,二者之間有直接的對應關系。
(3).StringBuilder
StringBuilder提供瞭高效創建字符串的方法,由StringBuilder表示的字符串是可變的(非恒定的),在需要多處使用“+”連接字符串變量的時候,推薦使用StringBuilder來完成,最後調用其ToString()方法輸出。當調用瞭StringBuilder的ToString()方法之後,StringBuilder將返回其內部維護的一個字符串字段引用,如再次修改StringBuilder,它將會創建一個新的字符串,這時被修改的是新的字符串,原來已經返回的字符串才不會發生改變。
StringBuilder有兩個比較重要的內部字段,大傢需要掌握:
m_MaxCapacity:StringBuilder的最大容量,它規定瞭最多可以放置到
m_StringValue的字符個數,默認值為Int32.MaxValue。m_MaxCapacity一旦被指定就不能再更改。
m_StringValue:StringBuilder維護的一個字符數組串,實際上可以理解為一個字符串。StringBuilder重寫的Tostring()方法返回的就是這個字段。
到此這篇關於C# String字符串案例詳解的文章就介紹到這瞭,更多相關C# string字符串詳解內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- C#的String和StringBuilder詳解
- Java字符串的intern方法有何奧妙之處
- C#中Foreach循環遍歷的本質與枚舉器詳解
- 深入理解Java new String()方法
- Java-String類最全匯總(上篇)