從String類型發散想到的一些東西
值類型 引用類型
值類型表示存儲在棧上的類型,包括簡單類型(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,放到棧上。
從性能角度上,裝箱的性能損耗>拆箱的性能損耗。在實際運用中,我們要盡量避免裝箱和拆箱,這也是泛型類型出現後,一個非常大的作用就是避免了裝箱拆箱的大量操作。