.Net Discovery 系列之一–string從入門到精通(上)

來源:互聯網
上載者:User

  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"。
  好了,結合第一節所講到的字串恒定性與第二節所講到的駐留機制,來理解一下下面4行代碼吧:
    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種情況講解動態建立字串時,駐留機制的表現:

  字串常量串連
    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指令直接處理編譯器計算後的字串值,所以這種情況字串駐留機制有效!
  字串變數串連
    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"來自同一記憶體位址。

  好了,下面兩節將通過執行個體為大家展示string的內部秘密,大家可以通過它測試一下自己對string的瞭解程度,敬請期待!

   我是李鳴(Aicken) 請您繼續關注我的下一篇文章。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.