深入理解C#中的String


關於C#中的類型

在C#中類型分為值類型和引用類型,引用類型和值類型都繼承自System.Object類,幾乎所有的引用類型都直接從System.Object繼承,而值類型具體一點則繼承System.Object的子類,即繼承System.ValueType。而String類型卻有點特別,雖然它屬於引用類型,但是他的一些特性卻有點類似值類型。

關於C# String

1、不變性

我們先來看看一個例子:

static void Main(string[] args)
{
    string str1 = "string";
    string str2 = str1;
    Console.WriteLine(object.ReferenceEquals(str1, str2));
    str2 += "change";
    Console.WriteLine(object.ReferenceEquals(str1, str2));
    Console.ReadKey();
}

輸出結果是True、False。為什么呢?我們來看看IL。

.entrypoint
  // 代碼大小       48 (0x30)
  .maxstack  2
  .locals init ([0] string str1,
           [1] string str2)
  IL_0000:  nop
  IL_0001:  ldstr      "string"
  IL_0006:  stloc.0
  IL_0007:  ldloc.0
  IL_0008:  stloc.1
  IL_0009:  ldloc.0
  IL_000a:  ldloc.1
  IL_000b:  ceq
  IL_000d:  call       void [mscorlib]System.Console::WriteLine(bool)
  IL_0012:  nop
  IL_0013:  ldloc.1
  IL_0014:  ldstr      "change"
  IL_0019:  call       string [mscorlib]System.String::Concat(string,string) 
  IL_001e:  stloc.1
  IL_001f:  ldloc.0
  IL_0020:  ldloc.1
  IL_0021:  ceq
  IL_0023:  call       void [mscorlib]System.Console::WriteLine(bool)
  IL_0028:  nop
  IL_0029:  call       valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
  IL_002e:  pop
  IL_002f:  ret

+=在內部調用了Concat函數,將str2和"change"連接起來直接生成了一個新的字符串,和原來的字符串是不同的對象。Trim、Remove函數都是會直接生成一個新的對象,字符串一經定義,就不能改變。

其實字符串具有原子性(也就是不變性),任何改變字符串的值的行為都不會成功,只會創建一個新的字符串對象。在實際編程中,我們會大量的使用字符串,這樣就會導致不停地創建新的字符串對象和分配內存,可能導致垃圾回收器GC不停地進行垃圾回收,大大降低性能,並且伴隨着內存溢出的危險。所以.Net對字符串進行了的特殊的處理,這就是字符串駐留池。

在字符串駐留池,保存着字符串字面值和指向的引用。每次有新的字符串創建,都會在駐留池中查找是否存在字面值相同的字符串,如果存在就將其指向已經存在的字符串的引用,不存在就直接新建一個字符串,然后指向一個新的地址。

2、作為函數參數的處理

在函數的參數傳遞中,值類型直接拷貝變量保存的值,傳遞的是一個值得副本,而引用類型傳遞的是地址的一個副本,所以在函數中改變引用參數中屬性的值會直接改變函數外真實類型對象的值。

static void Main(string[] args)
{
    People people = new People() { Name = "Jack" };
    Console.WriteLine(people.Name);
    Change(people);
    Console.WriteLine(people.Name);
    Console.ReadKey();
}

static void Change(People p)
{
    p.Name = "Eason";
}

class People
{
    public string Name { get; set; }
}

程序先輸出Jack,后輸出Eason,可以說明引用類型傳遞的是引用地址,函數改變的參數對象和外部傳遞進來的對象是一個對象。

那么我們來看看String作為參數的情況:

static void Main(string[] args)
{
    string str = "string";
    Console.WriteLine(str);
    Change(str);
    Console.WriteLine(str);
    Console.ReadKey();
}

static void Change(string str)
{
    str = "change";
    Console.WriteLine(str);
}

結果輸出string、change、string。調用Change函數后str的值還是"string",由於字符串類型的不變性,在Change函數中對str進行賦值會重新創建一個新的字符串對象,然后為這個新的對象附上引用。所以雖然字符串類型是引用類型,但是在參數傳遞時它其實相當於值類型。

3、相等比較處理

先看一個例子:

string str1 = "string";
string str2 = "string";
string str3 = "stringstring";
string str4 = "string" + "string";
string str5 = str1 + "string";
Console.WriteLine(ReferenceEquals(str1, str2));
Console.WriteLine(str1 == str2);
Console.WriteLine(ReferenceEquals(str3, str4));
Console.WriteLine(str3 == str4);
Console.WriteLine(ReferenceEquals(str3, str5));
Console.WriteLine(str3 == str5);
Console.ReadKey();

不出意外結果都應該為True,True,True,True,True,True,但是結果卻是True,True,True,True,False,True,str3和str5不是一個對象,他們不是指向同一個地址,為什么呢?經過查看IL代碼發現,str5在IL代碼中調用了Concat函數將str1和"string"進行了拼接,那這個Concat函數到底做了什么。

public static string Concat(string str0, string str1)
{
    if (IsNullOrEmpty(str0))
    {
        if (IsNullOrEmpty(str1))
        {
            return Empty;
        }
        return str1;
    }
    if (IsNullOrEmpty(str1))
    {
        return str0;
    }
    int length = str0.Length;
    string dest = FastAllocateString(length + str1.Length);
    FillStringChecked(dest, 0, str0);
    FillStringChecked(dest, length, str1);
    return dest;
}

FastAllocateString函數負責分配長度為str0.Length+str1.Length的空字符串dest,FillStringChecked分別將str0和str1復制到dest中,最后生成由str0和str1連接成的字符串,這樣不會再去字符串駐留池中查找是否存在和dest相同的字符串,而是直接生成一個新的對象。所以字符串變量和字符串常量進行拼接后會直接生成一個新的對象,繞過駐留池檢查。

而字符串常量拼接不會產生新的字符串,除非駐留池中沒有與之拼接后字面值相等的字符串。我們來看看IL代碼:

  IL_0001:  ldstr      "string"
  IL_0006:  stloc.0
  IL_0007:  ldstr      "string"
  IL_000c:  stloc.1
  IL_000d:  ldstr      "stringstring"
  IL_0012:  stloc.2
  IL_0013:  ldstr      "stringstring"
  IL_0018:  stloc.3
  IL_0019:  ldloc.0
  IL_001a:  ldstr      "string"
  IL_001f:  call       string [mscorlib]System.String::Concat(string,string)
  IL_0024:  stloc.s    str5
  IL_0026:  ldloc.0
  IL_0027:  ldloc.1

str3和str4的字面值是相等的,都是"stringstring",str3先於str4被初始化,當str4被初始化的時候,由於其字面值和str3相等,所以CLR會將str3指向的地址賦給str4,所以str3和str4引用是相等的。

至於""操作符的得到的結果都是True是因為""操作符會調用String.Equal方法,IL代碼如下:

  IL_0032:  call       bool [mscorlib]System.String::op_Equality(string,string)

op_Equality最終會調用String.Equal函數,Equal函數的比較步驟是先比較兩個對象的引用是否相等,不相等的話再對值進行比較,比較值時是按位比較的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM