程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> .Net Discovery系列之一 string從入門到精通(上)

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

編輯:關於.NET

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的了解程度,敬請期待!

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved