值類型表示存儲(chǔ)在棧上的類型,包括簡(jiǎn)單類型(int、long、double、short)、枚舉、struct定義;
引用類型表示存在堆上的類型,包括數(shù)組、接口、委托、class定義;
string 是引用類型
不可變性。字符串創(chuàng)建后,重新賦值的話,不會(huì)更新原有值,而是將引用地址更新到一個(gè)新的內(nèi)存地址上。
留存性。.NET運(yùn)行時(shí)有個(gè)字符串常量池的概念,在編譯時(shí),會(huì)將程序集中所有字符串定義集中到一個(gè)內(nèi)存池中,新定義的字符串會(huì)優(yōu)先去常量池中查看是否已存在,如果存在,則直接引用已存在的字符串,否則會(huì)去堆上重新申請(qǐng)內(nèi)存創(chuàng)建一個(gè)字符串。
下面是關(guān)于字符串的一些單元測(cè)試,仔細(xì)觀察下各個(gè)不同:
[Fact] public void Base_Test() { string a = "abc"; string b = "abc"; //字符串的留存性,初始化后會(huì)放入常量池,b直接引用a的對(duì)象 Assert.True(string.ReferenceEquals(a, b)); string c = new String("abc"); string d = new String("abc"); //直接new的話,會(huì)重新分配內(nèi)存 Assert.False(string.ReferenceEquals(c, d)); Assert.False(string.ReferenceEquals(a, c)); string e = "abc"; //這里e還是使用字符串的留存性,且使用的還是a的地址。證明c分配的內(nèi)存引用并沒(méi)有放入常量池替換 Assert.True(string.ReferenceEquals(a, e)); Assert.False(string.ReferenceEquals(c, e)); string f = "abc" + "abc"; string g = a + b; string h = "abcabc"; //f在編譯期間確定,實(shí)際還是從常量池中獲取 //IsInterned 表示從常量池中獲取對(duì)應(yīng)的字符串,獲取失敗返回null //a+b實(shí)際上是發(fā)生了字符串組合運(yùn)算,內(nèi)部重新new了一個(gè)新的字符串,所以f,g引用地址不同 Assert.False(string.ReferenceEquals(f, g)); Assert.True(string.ReferenceEquals(string.IsInterned(f), h)); Assert.True(string.ReferenceEquals(f, h)); }
字符串拼接是一個(gè)非常耗資源的操作,例如 string a="b"+"c"
,實(shí)際上創(chuàng)建了3個(gè)字符串"b"、"c"、"bc"。所以在這個(gè)時(shí)候就需要StringBuilder來(lái)專門執(zhí)行字符串拼接操作了。
那么StringBuilder是如何實(shí)現(xiàn)的呢?
實(shí)際上StringBuilder內(nèi)部維護(hù)了一個(gè)char數(shù)組,所有的appned類的操作都是將字符串轉(zhuǎn)化為char存入數(shù)組。最后ToString()的時(shí)候才去組裝string,減少了大量中間string的創(chuàng)建,是非常高效的字符串組裝工具。
StringBuilder內(nèi)部還有一個(gè) Capacity
屬性,用于定義數(shù)組的初始容量,默認(rèn)值為25。超過(guò)容量會(huì)觸發(fā)擴(kuò)容操作。所以在實(shí)際操作中,如果我們能預(yù)估到拼接字符串的長(zhǎng)度,在定義StringBuilder給 Capacity
屬性附上一個(gè)合理的值,將會(huì)有更加高效的性能。
equals:比較字符串的值
==:比較字符串的引用地址是否相同
首先有個(gè)前提,我們所看到的equals,==,來(lái)自于System.Object對(duì)象,幾乎所有的原生對(duì)象都對(duì)其進(jìn)行了重寫,才構(gòu)成了我們目前的認(rèn)知。重寫equals必須重寫GetHashCode。官方給出重寫的實(shí)現(xiàn)約定如下:
Equals每個(gè)實(shí)現(xiàn)都必須遵循以下約定:
自反性(Reflexive): x.equals(x)必須返回true.
對(duì)稱性(Symmetric): x.equals(y)為true時(shí),y.equals(x)也為true.
傳遞性(Transitive): 對(duì)于任何非null的應(yīng)用值x,y和z,如果x.equals(y)返回true,并且y.equals(z)也返回true,那么x.equals(z)必須返回true.
一致性(Consistence): 如果多次將對(duì)象與另一個(gè)對(duì)象比較,結(jié)果始終相同.只要未修改x和y的應(yīng)用對(duì)象,x.equals(y)連續(xù)調(diào)用x.equals(y)返回相同的值l.
非null(Non-null): 如果x不是null,y為null,則x.equals(y)必須為false
GetHashCode:
兩個(gè)相等對(duì)象根據(jù)equals方法比較時(shí)相等,那么這兩個(gè)對(duì)象中任意一個(gè)對(duì)象的hashcode方法都必須產(chǎn)生同樣的整數(shù)。
在我們未對(duì)對(duì)象進(jìn)行修改時(shí),多次調(diào)用hashcode使用返回同一個(gè)整數(shù).在同一個(gè)應(yīng)用程序中多次執(zhí)行,每次執(zhí)行返回的整數(shù)可以不一致.
如果兩個(gè)對(duì)象根據(jù)equals方法比較不相等時(shí),那么調(diào)用這兩個(gè)對(duì)象中任意一個(gè)對(duì)象的hashcode方法,不一同的整數(shù)。但不同的對(duì)象,產(chǎn)生不同整數(shù),有可能提高散列表的性能.
請(qǐng)慎重重寫Equals和GetHashCode??!重寫Equals方法必須要重寫GetHashCode!!
關(guān)于equals方法參數(shù) StringComparison
public enum StringComparison { // // 摘要: // 使用區(qū)分區(qū)域性的排序規(guī)則和當(dāng)前區(qū)域性比較字符串。 CurrentCulture = 0, // // 摘要: // 通過(guò)使用區(qū)分區(qū)域性的排序規(guī)則、當(dāng)前區(qū)域性,并忽略所比較的字符串的大小寫,來(lái)比較字符串。 CurrentCultureIgnoreCase = 1, // // 摘要: // 使用區(qū)分區(qū)域性的排序規(guī)則和固定區(qū)域性比較字符串。 InvariantCulture = 2, // // 摘要: // 通過(guò)使用區(qū)分區(qū)域性的排序規(guī)則、固定區(qū)域性,并忽略所比較的字符串的大小寫,來(lái)比較字符串。 InvariantCultureIgnoreCase = 3, // // 摘要: // 使用序號(hào)(二進(jìn)制)排序規(guī)則比較字符串。 Ordinal = 4, // // 摘要: // 通過(guò)使用序號(hào)(二進(jìn)制)區(qū)分區(qū)域性的排序規(guī)則并忽略所比較的字符串的大小寫,來(lái)比較字符串。 OrdinalIgnoreCase = 5 }
通常情況下最好使用 Ordinal或者OrdinalIgnoreCase,性能上最為高效。
除非有特殊的需要,不要使用 InvariantCulture或者InvariantCultureIgnoreCase,因?yàn)樗紤]所有Culture的字符轉(zhuǎn)化對(duì)比情況,性能是極差的。
CurrentCulture和CurrentCultureIgnoreCase由于只有本地Culture對(duì)比,所以性能還可以接受。
首先關(guān)于參數(shù)的存儲(chǔ),參數(shù)是存在棧上的。傳遞參數(shù)時(shí),會(huì)將對(duì)象的“值”在棧copy一份,然后將副本的值傳給方法。對(duì)象參數(shù)的傳遞分為兩種 “值傳遞”和“引用傳遞”。(注意這里的引號(hào))
值傳遞。默認(rèn)的參數(shù)傳遞都是這種方式。會(huì)將對(duì)象的值在棧copy一份,然后將復(fù)制集的值傳給方法。這里的值對(duì)于 值類型來(lái)說(shuō),即為對(duì)象副本的值。對(duì)于引用類型來(lái)說(shuō),即為對(duì)象在堆上的地址。
引用傳遞??梢酝ㄟ^(guò) ref
out
關(guān)鍵字實(shí)現(xiàn)。對(duì)于值類型,會(huì)直接傳入原對(duì)象在棧上的引用。對(duì)于引用類型,會(huì)傳入原有對(duì)象的堆地址的引用。
這里string雖然是引用類型,但是產(chǎn)生的效果缺和值類型參數(shù)傳遞一樣的。大家參考上面關(guān)于string的特性思考下原因。
靜心慢慢回味下列單元測(cè)試
[Fact] public void Base_Test() { //引用類型參數(shù) TestClass s = new TestClass(); s.Tag = "abc"; TestMethod m = new TestMethod(); m.ReNew(s); //參數(shù)s 實(shí)際是對(duì)象 s的 地址拷貝。兩者在棧上不同,但是指向的堆地址相同 //在ReNew方法中 "參數(shù)s" 重新指向了一個(gè)新的對(duì)象,但是不影響舊的對(duì)象s Assert.True(string.Equals("abc", s.Tag)); m.Change(s, "123"); //Change方法是直接修改 參數(shù)s 指向的堆對(duì)象內(nèi)的字段數(shù)據(jù),所有對(duì)象s字段也發(fā)生了變化 Assert.True(string.Equals("123", s.Tag)); m.ReNew2(ref s); //注意和ReNew的區(qū)別,因?yàn)槭莚ef 引用傳遞,所有原對(duì)象引用地址指向了新new的對(duì)象地址 Assert.False(string.Equals("abc", s.Tag)); Assert.True(string.Equals("cba", s.Tag)); //值類型參數(shù) int val = 100; //Change方法內(nèi)部改變了val的值,但不影響val原來(lái)的值 m.Change(val); Assert.True(val == 100); m.Change(out val); //使用out標(biāo)記,改變了val原來(lái)的值 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都是用來(lái)標(biāo)識(shí)通過(guò)引用傳遞方式傳參。不同的是,ref 需要參數(shù)在方法調(diào)用前初始化,out 則要求參數(shù)在方法體內(nèi)賦值。
裝箱,即值類型轉(zhuǎn)化為引用類型;從內(nèi)存存儲(chǔ)角度,將值類型從棧的值copy,然后放到堆上,并附加額外的引用類型功能內(nèi)存占用(如類型指針、同步塊索引等)。
拆箱,即引用類型轉(zhuǎn)化為值類型。從內(nèi)存存儲(chǔ)角度,獲取引用類型的指針,得到值copy,放到棧上。
從性能角度上,裝箱的性能損耗>拆箱的性能損耗。在實(shí)際運(yùn)用中,我們要盡量避免裝箱和拆箱,這也是泛型類型出現(xiàn)后,一個(gè)非常大的作用就是避免了裝箱拆箱的大量操作。
聯(lián)系客服