從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
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,放到棧上。
從效能角度上,裝箱的效能損耗>拆箱的效能損耗。在實際運用中,我們要儘量避免裝箱和拆箱,這也是泛型型別出現後,一個非常大的作用就是避免了裝箱拆箱的大量操作。