歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux編程 >> Linux編程 >> C#基礎知識之字符串

C#基礎知識之字符串

日期:2017/3/1 9:09:50   编辑:Linux編程

string作為我們在編程當中用的最多的數據類型,同時又由於它的特殊性,怎麼強調它的重要性都不為過,理解string的一些類型和存儲機制,有助於我們寫出正確且高效的代碼.

一.string類型

1.string的類型

string類型直接繼承Object類型,Object類型是引用類型,因而string類型是引用類型無疑.

我們借助VS的類視圖可以看到這一點:

這意味著:

(a).string類型不會在線程的堆棧中存儲任何字符串,而是存儲在堆上

(b).未初始時,它被設置為null

PS:在內部,string是用字符串char的集合來維護的

2.string聲明的IL描述

在IL中,構造新實例的IL指令是newobj,是不是string也是這樣?

我們使用如下代碼:

 1 class Program
 2 {
 3      static void Main(string[] args)
 4     {
 5          string str = "Hello World!";
 6          string str2 = "Hello" + " My" + " World!";
 7           Person person = new Person();
 8      }
 9  }
10 
11 class Person
12 {
13     string Name;
14 }

我們查看IL代碼如下:

可以看出

(a).對比1和3,構造Person對象使用了newobj指令,但是在構造字符串的時候,使用了專門的ldstr(load string)指令

(b).更進一步,編譯器將這些字面值字符串放到模塊的元數據中,在運行時加載和引用它們

(c).看2,對於使用+符合將各literal連接起來的寫法,編譯器在編譯的過程中會直接連接他們.

二.string的操作帶來的疑問

OK,通過第1部分,我們知道了,string是引用類型,它存儲在堆中.

我們知道對於引用類型,賦值操作=會傳遞的是引用,不是值,但構造不同的引用類型時通常它們的引用也不同.如下面這種:

 1 class Program
 2 {
 3      static void Main(string[] args)
 4      {
 5            //Person實害?例
 6            Person person1 = new Person("A");
 7            Person person2 = new Person("A");
 8            Console.WriteLine(object.ReferenceEquals(person1, person2));
 9 
10            //string
11            string str1 = "Hello World!";
12            string str2 = "Hello World!";
13            string str3 = "Hello " + "World!";
14            Console.WriteLine(object.ReferenceEquals(str1, str2));
15            Console.WriteLine(object.ReferenceEquals(str1, str3));
16 
17            Console.Read();
18        }
19 }
20 
21class Person
22 {
23    public Person(string strName)
24    {
25 
26    }
27 } 

我們先給出運行結果:

我們知道object.ReferenceEquals是比較兩個對象的引用是否一樣,對於第1種Person的情況,我們可以理解,因為他們都是構造了不同的對象,引用的存儲地址也是不同的.但對於第2種,第3種,string就像成為了值類型一樣,返回了True,那麼問題來了:

A.在聲明的時候,string存儲的是什麼?

B.什麼原因使得兩個string的引用地址是一樣的?

這就引出了我們要討論的核心問題:字符串駐留.

三.字符串駐留

1.string存儲的是引用

string對象存儲的是引用,引用對象存儲在堆中,會生成一個對象,同時將這個對象的地址(引用)給堆棧去使用.也就是說兩個string引用了堆中同一塊對象.

2.字符串駐留讓兩個string的引用地址是一樣

在CLR初始化時,會創建一個Hash表,在這個表中,Key是字符串,值是字符串在堆中的地址.當聲明一個字符串的時候,會先去這個HashTable中去找是否存在這個Key,如果存在則返回對應的引用,如果不存在則納入HashTable.如下圖所示:

Step1:當執行語句string str1 = "Hello World!";時,str1拿到了Add1;

Step2:當執行語句string str2= "Hello World!";時,CLR會去HashTable中去找,找到,返回Add1給str2;

Step3:現在用object.ReferenceEquals比較str1和str2的引用,因為都是Add1,因而返回True.

我們現在通過內存分析工具ANTS Memory Profile來證明,字符串駐留機制是確實存在的.

代碼如下:

1 static void Main(string[] args)
2 {
3      Console.ReadLine();//第台?一?次?快ì照?位?置?
4      string str1 = "Hello World!";
5      string str2 = "Hello World!";
6      Console.ReadLine();//第台?二t次?快ì照?位?置?
7 }

加載兩次快照,對比差異:

我們可以看到,在這裡有一個string的實例進去了,而且整個過程當中,也只有這一個string實例進去了,我們可以進一步看下進去的內容是什麼.

我們在這裡發現了”Hello World!”字符串,並且只有一個.這也就從內存分析的角度證明了字符串駐留的存在.

3.駐留字符串的HashTable是不受GC管理,但表達式中存在variable時,則不駐留在HashTable

我們實驗如下:

 1 static void Main(string[] args)
 2 {
 3      Console.ReadLine();//第1次快照位置
 4      Test();
 5      GC.Collect();
 6      Console.ReadLine();//第3次快照位置
 7 }
 8 
 9 static void Test()
10 {
11      string str1 = "Hello World!";
12      string str2 = "Hello World!" + str1;
13 Console.ReadLine();//第2次快照位置
14 }

第2次快照,我們可以看到:

進去了3個對象,分別是:byteIndex,”Hello World!”,”Hello World!Hello World!”

第3次快照是在調用了GC.Collect()後再進行的快照,以快照2為對比線,我們查看第3次快照.

我們看到,有一個對象被GC回收掉了,具體是什麼被回收了?我們再看:

現在只剩下byteIndex,”Hello World!”兩個對象,什麼被回收了呢?顯然是:”Hello World!Hello World!”

這也就證明了我們所說的:駐留字符串的HashTable是不受GC管理,但表達式中存在variable時,則不駐留在HashTable.

進一步:除非卸載AppDomain或進程終止,否則HashTable引用的string對象不能被釋放.

4.字符串的駐留是基於整個進程的

我們添加兩個不同的AppDomain,在各自的應用程���域中執行BuildString()方法,同時由於應用程序域之間本是不能訪問彼此對象的,我們使用"封送(Marshaling)"機制,封送又分為按值分送(主要采用序列化的方式)和按引用封送(如采用.Net Remoting).這裡,要實現按引用封送,Test類繼承MarshalByRefObject類.

測試代碼

class Program
{
     static void Main(string[] args)
     {
           Console.ReadLine();
            AppDomain domina1 = AppDomain.CreateDomain("First");
            Test t1 = (Test)domina1.CreateInstanceAndUnwrap(typeof(Test).Assembly.FullName, typeof(Test).FullName);
            t1.BuildString();

            AppDomain domina2 = AppDomain.CreateDomain("Second");
            Test t2 = (Test)domina1.CreateInstanceAndUnwrap(typeof(Test).Assembly.FullName, typeof(Test).FullName);
            t2.BuildString();

            Console.ReadLine();

        }
    }

public class Test : MarshalByRefObject
{
     public void BuildString()
     {
          var str1 = "Hello";
          var str2 = "Hello";
          var str3 = "World";
          var str4 = "World";
       }
}

我們拿到兩張快照,在第1張跟第2張快照對比後我們發現:

我們再具體查看內容(“World”字符串就不截圖了):

通過以上的分析,我們確信,字符串的駐留是基於整個進程的.

5.我們可以通過string.Intern方法來將字符串強制加入HashTable,也可以通過string.IsInterned來判斷字符串是否在HashTable中存在。

四.字符串池

在編譯時,編譯器會處理所有的literal字符串,並嵌入托管模塊的元數據中,但如果每次都寫入元數據,假設這個字符串在程序中多次出現,那就需要多次寫入元數據,這會使生成的文件無限地增大.

C#編譯器,只在元數據中將literal字符串寫入一次,將多個實例合並成一個實例,所有引用該字符串的代碼都被修改成引用元數據中的同一個字符串,這能顯著地減少生成文件的大小.這種特性,我們稱之為字符串池.

五.string的不可變性

string是不可變的,這意味著:

a.字符串一經創建便不能更改,不能變長、變短或修改其中的任何字符;

b.每次對於字符串的變更操作,如果是帶變量操作,都會在堆上生成新的字符串,並返回新的引用,會造成頻繁的GC回收,從而造成性能問題,如果不帶變量操作則會采用字符串駐留;

c.操作和訪問字符串不會發生線程同步問題,線程安全;

d.String類是sealed(密封)的,這是為了保護string的不可變性。

問題來了,如何實現string的不可變性呢?

string在內部是用char數組實現的,在char數據中,我們不可以改變數組的引用,但是我們可以直接修改char數組的值,為了實現string的不可變性,string在實現各種方法時,不會觸動char數組中的元素。

參見7.

六.StringBuilder:為解決string的性能而生

通過前面的內容我們可以知道,string容易產生性能問題,StringBuilder可以解決這個問題。

它的內部使用char[]來進行操作,默認為16,如果超過容量,則在堆中產生一個倍增容易的新char[]數組,復制字符,並開始使用新數組,前一個數組則被GC回收。如果不超過當前容量,是不是會產生一個新的char[]數組的。

使用ToString()方法也會在堆中產生一個新的對象。

七.總結

1.string是引用類型

2.string使用了字符串池來減少元數據文件的大小

3.string使用了字符串駐留來提升效率,駐留的字符串采用HashTable來存儲,它不受GC管轄,HashTable是基於進程共享的.

4.string是不可變的,由此帶來的性能問題,可以通過StringBuilder來解決.

Copyright © Linux教程網 All Rights Reserved