1. 程式人生 > >C# string 字串詳解 恆定 駐留

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 字串 恆定 駐留

string是一種很特殊的資料型別,它既是基元型別又是引用型別,在編譯以及執行時,.Net都對它做了一些優化工作,正式這些優化工作有時會迷惑程式設計人員,使string看起來難以琢磨。這篇文章共四節,來講講關於string的陌生一面。  一.恆定的字串 要想比較全面的瞭解stirng型別,首先要清楚.Ne

C++ string 字串

C++之所以拋棄char*的字串而選用C++標準程式庫中的string類,是因為他和前者比較起來,不必擔心記憶體是否足夠、字串長度等等,而且作為一個類出現,他整合的操作函式足以完成我們大多數情況下(甚至是100%)的需要。我們可以用= 進行賦值操作,== 進行比較,+ 做串

C++ string 用法--原作者是Nicolai M.Josuttis

任何人對本文進行引用都要標明作者是Nicolai M.Josuttis /////////////////////////////////////////////////////////////////////////////////// C++ 語言是個十分優秀的語言,但優秀並不表

String 字串 / 常用API

String 詳解 / 常用API 簡介   String 是不可改變的字串序列。String 為字串常量   StringBuilder 與StringBuffer 均為可改變的字串序列。為字串變數     StringBuilder 是非執行緒安全的 (JDK 1.5)     StringBu

C++ string CString

C++ string 詳解 前言: string 的角色 1 string 使用 1.1 充分使用string 操作符 1.2 眼花繚亂的string find 函式 1.3 string insert, replace, erase 2 string 和 C風格字串 3

c++string函式

首先,為了在我們的程式中使用string型別,我們必須包含標頭檔案 。如下:    #include //注意這裡不是string.h string.h是C字串標頭檔案 1.宣告一個C++字串 宣告一個字串變數很簡單:    string Str; 這樣我們就聲明瞭一個字串變數,但既然是一個類,就

String字串

參考文章 事件描述 由於公司的需要需要從日誌中拉取一些重要的資料,於是通過readLine方法來獲取資料,一個日誌檔案的大小500M-1G不等。於是思考,String字串不是放常量池的嗎?常連池是放在方法區,方法區屬於永久區。那麼如果這個專案當做一個

C語言字串

1.1       字元陣列定義 char array[100] //定義一個長一百位元組長度的字串 1.2       字元陣列初始化 char array[100] = { 'a','b','c' };//初始化 array 此時array = "abc"

C++ string 用法

任何人對本文進行引用都要標明作者是Nicolai M.Josuttis /////////////////////////////////////////////////////////////////////////////////// C++ 語言是個十分優秀的語言,但

c++ string使用

之所以拋棄char*的字串而選用C++標準程式庫中的string類,是因為他和前者比較起來,不必擔心記憶體是否足夠、字串長度等等,而且作為一個類出現,他整合的操作函式足以完成我們大多數情況下(甚至是100%)的需要。我們可以用 = 進行賦值操作,== 進行比較,+ 做串聯(

C++ string

常見 連續 可見 ras def 位置 primer 問題 iter   字符串是存儲在內存的連續字節中的一系列字符。C++ 處理字符串的方式有兩種,一種來自 C 語言,常被稱為 C-風格字符串,另一種是基於 string 類庫的字符串處理方式。C 風格字符串的處理可以參考

C++中string(轉載)(最下面有程式碼實現)

作者:yzl_rex 來源:CSDN 原文:https://blog.csdn.net/yzl_rex/article/details/7839379 要想使用標準C++中string類,必須要包含 #include < string>// 注意是< string>

C++ STL string 用法

一、string的初始化 首先,為了在程式中使用string型別,必須包含標頭檔案 <string>。如下: #include <string> 注意這裡不是string.h,string.h是C字串標頭檔案。 string類是一個模板類

C++附加知識---string型別

一、string簡介 概念:string表示可變長的字元序列,string作為標準庫的一部分 使用前需要匯入標頭檔案與名稱空間,string定義在std中 #include <string> using std::string; 二、string物件的初始

C++之string型別

c++字元陣列和字串的區別: 唯一的區別就是字串末尾有一個'\0'而字元陣列是沒有的. char ch1[]="hello";//該串有6個字元,5個可見字元,因為是串,所以末尾自動加上'\0';共六個char ch2[]={'h','e','l','l','o'};

C# ListView用法

ont 結束 server 發生 匹配 鼠標 之前 小圖標 order 一、ListView類 1、常用的基本屬性: (1)FullRowSelect:設置是否行選擇模式。(默認為false) 提示:只有在Details視圖該屬性才有意義

C# Graphics類

一段 特定 roc sta 指定順序 rendering 嘗試 osi intersect Brush 類.NET Framework 4定義用於填充圖形形狀(如矩形、橢圓、餅形、多邊形和封閉路徑)的內部的對象。 屬於命名空間: System.Drawing這是一個抽象基

C# ListView用法(轉)

分組 創建 cti 排列 checkbox 定義 com 程序 erl 一、ListView類 1、常用的基本屬性: (1)FullRowSelect:設置是否行選擇模式。(默認為false) 提示:只有在Details視圖該屬性才有

【轉】讓你不再害怕指針——C指針(經典,非常詳細)

有一個 情況 value 第一個字符 接下來 意思 strcpy abcdefg 數值 前言:復雜類型說明 要了解指針,多多少少會出現一些比較復雜的類型,所以我先介紹一下如何完全理解一個復雜類型,要理解復雜類型其實很簡單,一個類型裏會出現很多運算符,他們也像普通的表

String

方法 類型 nal 提高 () masm static 基本類型 代碼 StringBuilder與StringBuffer的功能基本相同,不同之處在於StringBuilder是非線程安全的,而StringBuffer是線程安全的,因此效率上StringBuilder類更