看是簡單并不簡單:C#基礎之Equals和Dispose
1.equal()和運算符==的區(qū)別
由于C#中有值類型和引用類型,那么相等也分為值相等和引用相等。先來看一個值類型簡單的例子,順便也寫了string類型的比較。
static void Main(string[] args){ int n1 = 1; int n2 = 1;Console.WriteLine(n1==n2);Console.WriteLine(n1.Equals(n2)); string str1 = "test"; string str2 = "test";Console.WriteLine(str1==str2);Console.WriteLine(str1.Equals(str2));Console.ReadKey(); //結果是4個 True}
前2個結果為true我可以理解,但是后2個為true我有點懷疑。一直記得不斷有人說string是特殊的引用類型,那這里應該是為str1和str2都分配了內(nèi)存并str1和str2指向各自的內(nèi)存,但是結果卻是true。我感覺很有可能是因為string類的“特殊”,才發(fā)現(xiàn)自己一直都沒有理解string類的特殊。百度之后發(fā)現(xiàn)原來是.NET做的優(yōu)化,由于str1和str2內(nèi)容相同,為了節(jié)省內(nèi)存故讓str1和str2指向同一個內(nèi)存區(qū)域,這樣就不用為str2開辟空間了。這種技術是字符串駐留技術,當CLR初始化時,會創(chuàng)建一個內(nèi)部的散列表,鍵為字符串,值Wie指向堆中字符串的引用,JIT編譯方法時,添加str1和“test”到空的散列表里了,str2賦值時會先看散列表里有沒有相等的,有的就直接指向相等的值的引用。編程中當把string作為參數(shù)傳遞時不會改變原有string對象的值,因為每次賦值會另外開辟內(nèi)存,這就是string引用類型的特殊之處。
對于引用類型,==比較的是兩個引用是不是指向同一個內(nèi)存地址,equal()方法比較的是兩個對象指向的內(nèi)存空間內(nèi)容是否相同。下面是關于引用類型的代碼。
class Program{ static void Main(string[] args){ //前面已提過這里str1和str2指向同一個引用,故obj1和obj2表示的地址是相同的string str1 = "test"; string str2 = "test"; object obj1 = str1; object obj2 = str2;Console.WriteLine(obj1 == obj2); //TrueConsole.WriteLine(obj1.Equals(obj2)); //True //這種情況的賦值內(nèi)存沒有做優(yōu)化,故obj3和obj4表示的是不一樣的地址string str3 = new string(new char[] { 't', 'e', 's', 't' }); string str4 = new string(new char[] { 't', 'e', 's', 't' }); object obj3 = str3; object obj4 = str4;Console.WriteLine(obj3 == obj4); //FalseConsole.WriteLine(obj3.Equals(obj4)); //True //當用new開辟新的空間創(chuàng)建對象時,people1和people2肯定是指向了不同的內(nèi)存地址 //而且這是2個不同的對象,我們不能只看它們有相同的方法相同的字段,還要看它們的標識等環(huán)境變量, //對于people3和peop4來說則是指向了同一塊內(nèi)存區(qū)域People people1 = new People("小方");People people2 = new People("小方");Console.WriteLine(people1 == people2); //FalseConsole.WriteLine(people1.Equals(people2)); //FalsePeople people3 = new People("小白");People people4 = people3;Console.WriteLine(people3 == people4); //TrueConsole.WriteLine(people3.Equals(people4)); //True Console.ReadKey();}} class People{ string name = null; public string Name{ get { return name; } set { name = value; }} public People(string strName){name = strName;}}
引用類型的變量是存在棧中,但指向的是堆中的地址。==操作符比較的是2個變量的值是否相等,那對于引用類型也就是比較它們表示的地址是否相同。equal表示的是2個變量指向的堆中的內(nèi)容是否相等。
2.思考:為什么要封裝字段
在上面的例子中,已經(jīng)形成習慣將字段封裝成屬性。我發(fā)現(xiàn)學習過程中大家總是這么寫,我有時候會想一下為什么要這樣寫,但可能還沒有做過實際的項目開發(fā)(沒有因為不這樣寫被坑過),所有我一直沒有想到最本質(zhì)的原因。因為經(jīng)常說為了保護數(shù)據(jù)的安全性不讓直接讀和寫,可是常常看到屬性中是既有set又有get的,那這樣還不是直接讀和寫,這樣有什么區(qū)別呢?今天既然又發(fā)現(xiàn)這個問題,不能還不解決了。查閱了前輩的經(jīng)驗后,我發(fā)現(xiàn)有2點說到本質(zhì)了:
1:安全性,也就是我可以在屬性中進行判斷,比如age,我可以在set的時候加一個if判斷范圍在0到150,雖然也可以在外部判斷,但這樣寫是一定不會出錯的!
2:當出現(xiàn)修改時,我們可以在屬性中進行修改,而不必修改整個程序。舉個例子,比如業(yè)務需求改變?yōu)槲覀兘o一個變量賦值,get時不是輸出原來的值,而是輸出這個值乘以2,如果該字段是public,整個程序用到該字段的地方都要修改,然而如果有屬性那就很好辦了,只需在get時乘以2就可以了。
3.Dispose()、Close()、Finalize()的區(qū)別
Close()和Dispose()差不多,只是因為Close這個詞更加容易理解,所以在Close()方法內(nèi)部調(diào)用了Dispose()方法。Dispose方法在內(nèi)部是去調(diào)用一個virtual的Dispose(bool)函數(shù)去釋放資源。具有Dispose()方法的類是實現(xiàn)了IDisposable接口的,很多類實現(xiàn)了IDisposable接口,但是只提供Close(),而不對外提供Dispose()。原因是這些類是顯示實現(xiàn)接口,這樣的話實現(xiàn)類對象將無法調(diào)用Dispose(),比如ClassA實現(xiàn)接口IDisposable接口,如果要調(diào)用Dispose方法則只能調(diào)用((IDisposable)new ClassA()).Dispose()。這樣做的目的也就是提供易于理解的Close()。例外有時候調(diào)用Close后我們還可以復活對象,而Dispose一旦被調(diào)用就會實實在在的釋放資源。
其實.NET最基本的釋放資源方法是Finalize和Dispose這2個方法,F(xiàn)inalize是用于釋放非托管資源的,Dispose可以釋放所有資源,即可釋放托管資源,又可以釋放非托管資源。我們程序員是無法顯示調(diào)用Finalize方法的,這個方法當我們使用析構函數(shù)時才能被調(diào)用,但是程序員并不會知道什么時候會調(diào)用析構函數(shù),這將由垃圾回收器控制。只有當垃圾回收器認為對象符合析構的時候才會調(diào)用,程序退出時也會調(diào)用析構函數(shù)。為了提升性能,我們最好不要使用空的析構函數(shù),因為如果類包括析構函數(shù),則Finalize隊列中則會創(chuàng)建一個成員選項,GC處理這個隊列時如果選項內(nèi)容為空那只會導致不必要的性能。
寫到這里我心里有2個疑問,上面提到建議我們不要使用空的析構函數(shù),可以理解是因為要提升性能,可是我寫析構函數(shù)不就是為了調(diào)用Finalize方法釋放資源嗎?還有既然Dispose方法可以釋放所有資源,何必還要寫一個Finalize方法呢?直到敲了前輩們寫的代碼才有了答案。
public class People : IDisposable{ //前面我們說了析構函數(shù)實際上是重寫了 System.Object 中的虛方法 Finalize, 默認情況下,一個類是沒有析構函數(shù)的,也就是說,對象被垃圾回收時不會被調(diào)用Finalize方法~People(){ // 必須以Dispose(false)方式調(diào)用,以false告訴Dispose(bool disposing)函數(shù)是從垃圾回收器在調(diào)用Finalize時調(diào)用的Dispose(false);} // 無法被客戶直接調(diào)用// 如果 disposing 是 true, 那么這個方法是被客戶直接調(diào)用的,那么托管的,和非托管的資源都可以釋放// 如果 disposing 是 false, 那么函數(shù)是從垃圾回收器在調(diào)用Finalize時調(diào)用的,此時會釋放托管資源protected virtual void Dispose(bool disposing){ // 那么這個方法是被客戶直接調(diào)用的,那么托管的,和非托管的資源都可以釋放if (disposing){ // 釋放 托管資源//...... } //釋放非托管資源//...... // 那么這個方法是被客戶直接調(diào)用的,告訴垃圾回收器從Finalization隊列中清除自己,從而阻止垃圾回收器調(diào)用Finalize方法.if (disposing)GC.SuppressFinalize(this);} //可以被客戶直接調(diào)用public void Dispose(){Dispose(true);}}
從上面的例子中可以看出Finalize和Dispose釋放了什么資源,而且如果是Dispose方式釋放資源后,還會調(diào)用GC.SuppressFinalize(this),這個方法會告訴GC沒必要再調(diào)用析構函數(shù)了,因為已經(jīng)使用Dispose方法釋放資源了。這說明析構函數(shù)只是作為資源釋放的一種補救措施,同樣的我們可以在析構函數(shù)中寫釋放非托管資源的代碼,最后再調(diào)用Finalize方法以保證資源被完全釋放,這樣就萬無一失了!
筆者Dispose還沒有充分理解。

關注公眾號↑↑↑:DotNet開發(fā)跳槽?
