從String型別發散想到的一些東西

炸雞啤酒不可負發表於2020-09-25

值型別 引用型別

值型別表示儲存在棧上的型別,包括簡單型別(int、long、double、short)、列舉、struct定義;
引用型別表示存在堆上的型別,包括陣列、介面、委託、class定義;
string 是引用型別

字元特殊性

  • 不可變性。字串建立後,重新賦值的話,不會更新原有值,而是將引用地址更新到一個新的記憶體地址上。

留存性。.NET執行時有個字串常量池的概念,在編譯時,會將程式集中所有字串定義集中到一個記憶體池中,新定義的字串會優先去常量池中檢視是否已存在,如果存在,則直接引用已存在的字串,否則會去堆上重新申請記憶體建立一個字串。
下面是關於字串的一些單元測試,仔細觀察下各個不同:
  [Fact]   public void Base_Test()   {       string a = "abc";       string b = "abc";       //字串的留存性,初始化後會放入常量池,b直接引用a的物件       Assert.True(string.ReferenceEquals(a, b));        string c = new String("abc");       string d = new String("abc");       //直接new的話,會重新分配記憶體       Assert.False(string.ReferenceEquals(c, d));       Assert.False(string.ReferenceEquals(a, c));        string e = "abc";       //這裡e還是使用字串的留存性,且使用的還是a的地址。證明c分配的記憶體引用並沒有放入常量池替換       Assert.True(string.ReferenceEquals(a, e));       Assert.False(string.ReferenceEquals(c, e));        string f = "abc" + "abc";       string g = a + b;       string h = "abcabc";       //f在編譯期間確定,實際還是從常量池中獲取       //IsInterned 表示從常量池中獲取對應的字串,獲取失敗返回null       //a+b實際上是發生了字串組合運算,內部重新new了一個新的字串,所以f,g引用地址不同       Assert.False(string.ReferenceEquals(f, g));       Assert.True(string.ReferenceEquals(string.IsInterned(f), h));       Assert.True(string.ReferenceEquals(f, h));   }

Stringbuilder

字串拼接是一個非常耗資源的操作,例如  string a="b"+"c" ,實際上建立了3個字串"b"、"c"、"bc"。所以在這個時候就需要StringBuilder來專門執行字串拼接操作了。
那麼StringBuilder是如何實現的呢?
實際上StringBuilder內部維護了一個char陣列,所有的appned類的操作都是將字串轉化為char存入陣列。最後ToString()的時候才去組裝string,減少了大量中間string的建立,是非常高效的字串組裝工具。
StringBuilder內部還有一個  Capacity 屬性,用於定義陣列的初始容量,預設值為25。超過容量會觸發擴容操作。所以在實際操作中,如果我們能預估到拼接字串的長度,在定義StringBuilder給  Capacity 屬性附上一個合理的值,將會有更加高效的效能。

equals ==

  • equals:比較字串的值
  • ==:比較字串的引用地址是否相同
首先有個前提,我們所看到的equals,==,來自於System.Object物件,幾乎所有的原生物件都對其進行了重寫,才構成了我們目前的認知。重寫equals必須重寫GetHashCode。官方給出重寫的實現約定如下:
Equals每個實現都必須遵循以下約定:
  • 自反性(Reflexive): x.equals(x)必須返回true.
  • 對稱性(Symmetric): x.equals(y)為true時,y.equals(x)也為true.
  • 傳遞性(Transitive): 對於任何非null的應用值x,y和z,如果x.equals(y)返回true,並且y.equals(z)也返回true,那麼x.equals(z)必須返回true.
  • 一致性(Consistence): 如果多次將物件與另一個物件比較,結果始終相同.只要未修改x和y的應用物件,x.equals(y)連續呼叫x.equals(y)返回相同的值l.
  • 非null(Non-null): 如果x不是null,y為null,則x.equals(y)必須為false
GetHashCode:
  • 兩個相等物件根據equals方法比較時相等,那麼這兩個物件中任意一個物件的hashcode方法都必須產生同樣的整數。
  • 在我們未對物件進行修改時,多次呼叫hashcode使用返回同一個整數.在同一個應用程式中多次執行,每次執行返回的整數可以不一致.
  • 如果兩個物件根據equals方法比較不相等時,那麼呼叫這兩個物件中任意一個物件的hashcode方法,不一同的整數。但不同的物件,產生不同整數,有可能提高雜湊表的效能.
請慎重重寫Equals和GetHashCode!!重寫Equals方法必須要重寫GetHashCode!!
關於equals方法引數  StringComparison
public enum StringComparison {     //     // 摘要:     //     使用區分割槽域性的排序規則和當前區域性比較字串。     CurrentCulture = 0,     //     // 摘要:     //     透過使用區分割槽域性的排序規則、當前區域性,並忽略所比較的字串的大小寫,來比較字串。     CurrentCultureIgnoreCase = 1,     //     // 摘要:     //     使用區分割槽域性的排序規則和固定區域性比較字串。     InvariantCulture = 2,     //     // 摘要:     //     透過使用區分割槽域性的排序規則、固定區域性,並忽略所比較的字串的大小寫,來比較字串。     InvariantCultureIgnoreCase = 3,     //     // 摘要:     //     使用序號(二進位制)排序規則比較字串。     Ordinal = 4,     //     // 摘要:     //     透過使用序號(二進位制)區分割槽域性的排序規則並忽略所比較的字串的大小寫,來比較字串。     OrdinalIgnoreCase = 5 }
通常情況下最好使用 Ordinal或者OrdinalIgnoreCase,效能上最為高效。
除非有特殊的需要,不要使用 InvariantCulture或者InvariantCultureIgnoreCase,因為它要考慮所有Culture的字元轉化對比情況,效能是極差的。
CurrentCulture和CurrentCultureIgnoreCase由於只有本地Culture對比,所以效能還可以接受。

引數傳遞

首先關於引數的儲存,引數是存在棧上的。傳遞引數時,會將物件的“值”在棧copy一份,然後將副本的值傳給方法。物件引數的傳遞分為兩種 “值傳遞”和“引用傳遞”。( 注意這裡的引號
  • 值傳遞。預設的引數傳遞都是這種方式。會將物件的值在棧copy一份,然後將複製集的值傳給方法。這裡的值對於 值型別來說,即為物件副本的值。對於引用型別來說,即為物件在堆上的地址。
  • 引用傳遞。可以透過  ref  out 關鍵字實現。對於值型別,會直接傳入原物件在棧上的引用。對於引用型別,會傳入原有物件的堆地址的引用。
這裡string雖然是引用型別,但是產生的效果缺和值型別引數傳遞一樣的。大家參考上面關於string的特性思考下原因。
靜心慢慢回味下列單元測試
    [Fact]     public void Base_Test()     {         //引用型別引數         TestClass s = new TestClass();         s.Tag = "abc";          TestMethod m = new TestMethod();         m.ReNew(s);         //引數s 實際是物件 s的 地址複製。兩者在棧上不同,但是指向的堆地址相同         //在ReNew方法中 "引數s" 重新指向了一個新的物件,但是不影響舊的物件s         Assert.True(string.Equals("abc", s.Tag));          m.Change(s, "123");         //Change方法是直接修改 引數s 指向的堆物件內的欄位資料,所有物件s欄位也發生了變化         Assert.True(string.Equals("123", s.Tag));          m.ReNew2(ref s);         //注意和ReNew的區別,因為是ref 引用傳遞,所有原物件引用地址指向了新new的物件地址         Assert.False(string.Equals("abc", s.Tag));         Assert.True(string.Equals("cba", s.Tag));          //值型別引數         int val = 100;         //Change方法內部改變了val的值,但不影響val原來的值         m.Change(val);         Assert.True(val == 100);          m.Change(out val);         //使用out標記,改變了val原來的值         Assert.True(val == 123);     } }  public class TestMethod {     public void ReNew(TestClass c)     {         c = new TestClass() { Tag = "cba" };     }      public void ReNew2(ref TestClass c)     {         c = new TestClass() { Tag = "cba" };     }      public void Change(TestClass c, string tag)     {         c.Tag = tag;     }      public void Change(int a)     {         a = 123;     }     public void Change(out int a)     {         a = 123;     } }  public class TestClass {     public string Tag { get; set; } }

ref out

ref out都是用來標識透過引用傳遞方式傳參。不同的是,ref 需要引數在方法呼叫前初始化,out 則要求引數在方法體內賦值。

裝箱 拆箱

裝箱,即值型別轉化為引用型別;從記憶體儲存角度,將值型別從棧的值copy,然後放到堆上,並附加額外的引用型別功能記憶體佔用(如型別指標、同步塊索引等)。
拆箱,即引用型別轉化為值型別。從記憶體儲存角度,獲取引用型別的指標,得到值copy,放到棧上。
從效能角度上,裝箱的效能損耗>拆箱的效能損耗。在實際運用中,我們要儘量避免裝箱和拆箱,這也是泛型型別出現後,一個非常大的作用就是避免了裝箱拆箱的大量操作。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69983799/viewspace-2724117/,如需轉載,請註明出處,否則將追究法律責任。

相關文章