程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> .NET,你忘記了麼?(六)——再談String

.NET,你忘記了麼?(六)——再談String

編輯:關於.NET

一. 文章伊始

在文章之前,說下寫出這篇文章的目的。在我昨天的一篇文章<<重溫設計模式(一 )——享元模式(Flyweight) >>中,我在文中提到了關於String的字符串駐留機制。 在文章的評論中,楊同學對我的字符串相關觀點提出質疑,並且成文,不過我現在無法找到 那個鏈接了。

於是,我想把這個老掉牙的話題在此重談。

究竟我們對String這個常用的類型有多少理解。

二. 從C看起

C語言是我接觸的第一個程序語言。還記得當時給我的C語言老師是一個專業做Java SOA的 老師。

於是,她在講授C的時候經常給我們時不時地與Java做著對比,盡管我們當時並不懂Java 是個什麼東東,只知道這個詞經常出現於手機游戲上。

當時我還記得老師一句很經典的話:我們要記得,C中沒有字符串這個概念(其實我們當 時還不懂什麼是字符串),所謂的字符串在C中表現為字符數組。

那就讓我們來復習一下,在C中的“字符串”的表現形式:

char s[]=”abc”;

接下來,我們便可以使用s去調用各種“字符串”函數。

那麼我們可以清楚地看到在C語言中,“字符串”其實存儲的就是字符數組的首地址,那 麼在.NET中又是如何呢?

三. String vs string

在學校的時候,這個問題被同學無數次問過,尤其是很多學Java的朋友。

string其實就是String的別名,當二者編譯為IL代碼時,二者並無區別,正如int之於 System.Int32。

二者的分別僅僅在於:

1. string是C#語言的基元類型,看起來更C#。

2.System.String是FCL的基元類型。

我常常是這樣來使用:

1. 如果涉及到語言的互操作,那麼毋容置疑,一定是System.String,不再贅述,如有問 題,請參考<<.NET,你忘記了麼?(一)——遵從CLS>>.

2. 如果只是聲明一段字符串,我會使用string,看上去可讀性更高,類似於你會使用int i=3;而很少見到System.Int32 i=3一樣。

3. 如果是涉及到使用字符串的靜態方法,那麼我常常使用System.String,因為String看 起來更像一個類。

四. 字符串的不變模式

我們在這裡先看例子:

static void Main(string[] args)
{
   string s1 = "Hello";
   string s2 = "Hello";
   Console.WriteLine(Object.ReferenceEquals(s1, s2));
   s2 = "Hello world";
   Console.WriteLine(Object.ReferenceEquals(s1, s2));
}

結果呢?

那為什麼s1和s2第一次的引用明明相等,可是經過一次改變卻又不同了呢?

那就讓我們來揭曉其中的秘密。

五. 深入字符串駐留

我們首先要先了解.NET的運行過程,如果還不太了解,請參考我的<<解析.NET運行 全程>>。

在CLR被加載之後,便SystemDomain所對應的托管堆中初始化了一個HashTable。這個 HashTable的目的就是為了存儲我們所創建過的字符串。

在Hashtable中,Key是string的內容,Value是這個字符串所對應的內存地址。

那我們來分析下上面的代碼,其實過程如下:

string s1=”Hello”;

string s2=”Hello”;

現在的s1和s2是指向同一塊地址,然後我們改變了s2的值,假設s2=”World”;那麼這個 時候:

當然,這個時候s1和s2就不指向同一塊引用地址了。

好,現在讓我們來系統的分析一下這個過程。

當初始化一個字符串時,會首先在這個系統初始化的Hashtable中查找每一個字符串常量 在Hashtable中是否存在,

如果不存在,那麼他便首先在托管堆中分配一塊內存地址來存儲這個字符串常量,然後在 Hashtable中增加一個鍵值對,將字符串的內容存儲為Key,而將分配的內存地址存儲為Value 。

如果存在,那麼就在將該引用指向原有的地址。下面的代碼進一步印證了這個觀點:

static void Main(string[] args)
{
   string s1 = "Hello";
   string s2 = "Hello";
   string s3 = "Hello world";
   Console.WriteLine("Compare s1 and s2:"+Object.ReferenceEquals(s1,  s2));
   s2 = "Hello world";
   Console.WriteLine("Compare s1 and s2:" + Object.ReferenceEquals(s1,  s2));
   Console.WriteLine("Compare s2 and s3:" + Object.ReferenceEquals(s2,  s3));
}

那麼當改 變該字符串值的時候又發生了什麼?這就是不變模式的精髓,我們也稱字符串橫定性。

六. 深入字符串恆定性

字符串橫定性是指一個字符串一經創建,就不可改變 。那麼也就是說當我們改變string值的時候,便會在托管堆上重新分配一塊新的內存空間, 而不會影響到原有的內存地址上所存儲的值。

其實這也就是為什麼說字符串是特殊的 引用類型的原因。他有著值類型的特點,也有著引用類型的特點。

微軟之所以這樣設 計我認為是出於兩點:

1. 保持字符串的恆定性意味著多用戶共享同一塊字符串地址 時不會出現線程同步的問題。

2. 如果沒有了字符串恆定,那麼字符串的駐留基本也 就無從實現了。

七. 追尋設計本源

微軟為什麼要這樣去設計字符串的結構呢 ?

我想這也是楊同學對我的觀點提出質疑的根本原因。

當然,本質當然是為 了節省內存。至於究竟這樣的內存節省會有多少。我從下面的例子來說明。

我們做一 個Web程序,成千上萬的用戶去訪問同一台服務器,String作為最常用的類型當然被頻繁使用 ,這毋容置疑。那麼在服務器中,同一個字符串就可能被初始化成千上萬,我們再提高用戶 量,那麼也許就是百萬級的數量,那麼這對服務器的內存是個多麼大的占用。

那麼, 有了字符串駐留,如果兩個字符串值相等的時候,就把兩個字符串指向同一塊對象,那麼這 樣是不是節省了相當多的內存呢?

至於楊同學的另外一個觀點,說難道在整個運行過 程中這個Hashtable都存在,並且保存著所有的數據麼?

我覺得這個是很容易進行反駁的。很明顯,Hashtable在整個運行過程都存在的,而數據 ,我個人認為應該是有兩種可能:

1. 定期去清理掉無用的鍵值對。

2. 像垃圾回收一樣,只有當存儲的字符串超過了Hashtable的大小,或者對定位Key的效 率造成一定影響時便對其進行回收。

八. String在IL上的特立獨行

我們都知道,在構造一個引用對象時,對應的IL代碼是newobj。

但是.NET為string准備了特殊的聲明方式:ldstr。該指令通過元數據中獲得的文本常量 來構造字符串對象。

補充一點,為什麼說是通過元數據中獲得的文本常量呢?因為C#將String作為了自己的基 元類型,於是編譯器會將這些文本常量存放在托管模塊的元數據中。

無論如何,這說明了String是個特殊的類型,CLR為他准備了更高效,特立獨行的方式。

九. String究竟是否享元?

String通過一個Hashtable來為自己保存一個緩存,然後每次初始化一個字符串時都先通 過搜索緩存找到是否存在可以重用的對象,然後進行重用或者創建新的實例。

而享元模式是采用共享技術解決大量細粒度對象的爆炸問題。

是不是一樣呢?

十. Intern方法

讓我們首先看看MSDN對Intern的解釋:

Intern:檢索系統對指定String的引用。如果存在這樣的字符串,那麼便返回他的引用, 否則便創建一個新的字符串,然後返回他的引用。

我為什麼在String那麼多的方法中單單鐘情於這個方法呢?讓我們先來看這段代碼:

static void Main(string[] args)
{
   string s1 = "Hello";
   string s2 = "HelloWorld";
   string s3 = s1 + "World";
   Console.WriteLine(Object.ReferenceEquals(s2,s3));
}

這也是當 時楊同學反駁我的一個理論依據。

原因就是在於s3是動態生成的字符串,這樣的字符 串是不會添加到緩存哈希表中進行維護到的。

那麼這個時候Intern就派上用場了。

讓我們來改寫一下上面的代碼:

static void Main(string[]  args)
{
  string s1 = "Hello";
  string s2 =  "HelloWorld";
  string s3 = s1 + "World";
   Console.WriteLine(Object.ReferenceEquals(s2,s3));
  s3 =  String.Intern(s3);
  Console.WriteLine(Object.ReferenceEquals(s2,  s3));
}

因為 Intern的作用是搜索整個Hashtable,然後去找是否有這個字符串的相關引用。於是他找到了 之後便會將這個引用返回給s3,那麼這個時候,當然s2和s3的引用便相等了。

而這個 方法的實現也讓我們來看下:

public static string Intern(string  str)
{
  if (str == null)
  {
    throw new  ArgumentNullException("str");
  }
  return  Thread.GetDomain().GetOrInternString(str);
}

這個方法究竟在實際 中有什麼用呢?讓我們向下看。

十一. 學以致用,雕蟲小技

我們在實際的應用中,字符串比較有著很大的應用,String.Compare()。

這個方法的本質是將整個string拆開,然後比較其中的每個字符。也就是在筆試題中常常 遇到的,我最常說的一句話就是:把字符串當成字符數組玩!

但是,我們知道,這樣是很損耗效率的。於是我們可以根據字符串的駐留特性去想想其他 方法。

在上文中,我頻繁的去使用Object.ReferenceEquals()。顧名思義,這個方法就是比較兩 個字符串的內存地址是否相等。由於字符串的駐留技術,因此兩個相等的字符串所指向的地 址是相等的。

因此我們可以使用Object.ReferenceEquals()來提高我們字符串比較的效率。

當然,很可能會出現某一字符串是動態生成的情況,那麼這個時候Intern就派上用場了, 當然,Intern同樣是個耗費效率的方法,因此如果我們只需要進行少量的比較操作,就沒有 必要使用這個方法了。

另外,就是說由於字符串的恆定不變性,我們在字符串拼接時,實際上就產生了大量的內 存垃圾。因此,如果要大量的字符串拼接,要使用StringBuilder類來完成,這個很多人都知 道,我就不詳細說這個了。

十二. 要點總結

String是個特殊的引用對象。

關鍵就在於他的字符串駐留技術和字符串的恆定不變性。

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