為什麼要用單例模式?

趙學智發表於2018-05-09

  我們在程式設計中最常用的模式就是單例模式了,然而單例模式都用在什麼場合?為什麼不用靜態方法而要用單例模式呢?要搞清這些問題,需要從靜態方法和非靜態方法的區別和聯絡說起。

 

  一、靜態方法常駐記憶體,非靜態方法只有使用的時候才分配記憶體?

  一般都認為是這樣,並且怕靜態方法佔用過多記憶體而建議使用非靜態方法,其實這個理解是錯誤的。

  為什麼會這樣,先從記憶體分配開始說起:

  託管堆的定義:對於32位的應用程式來說,應用程式完成程式初始化後,CLR將在程式的可用地址空間分配一塊保留的地址空間,它是程式(每個程式可使用4GB)中可用地址空間上的一塊記憶體區域,但並不對應任何實體記憶體,這塊地址空間即是託管堆。

  託管堆有分為多個區域,其中最重要的是垃圾回收堆(GC Heap)和載入堆(Loader Heap),GC Heap用於儲存物件例項,受GC管理;Loader Heap又分為High-Frequency Heap、Low-Frequency Heap和Stub Heap,不同的堆上又儲存不同的資訊。Loader Heap最重要的資訊就是後設資料相關的資訊,也就是Type物件,每個Type在Loader Heap上體現為一個Method Table(方法表),而Method Table中則記錄了儲存的後設資料資訊,例如基型別、靜態欄位、實現的介面、所有的方法等等。Loader Heap不受GC控制,其生命週期為從建立到AppDomain解除安裝。(摘自《你必須知道的.Net》)

  由此我們就明白了,靜態方法和非靜態方法,在記憶體裡其實都放在Method Table裡了,在一個類第一次被載入的時候,它會在Loader Heap裡把靜態方法,非靜態方法都寫入Method Table中,而且Loader Heap不受GC控制,所以一旦載入,GC就不會回收,直到AppDomain解除安裝

  由此我們也明白了,靜態方法和非靜態方法,他們都是在第一次載入後就常駐記憶體,所以方法本身在記憶體裡,沒有什麼區別,所以也就不存在”靜態方法常駐記憶體,非靜態方法只有使用的時候才分配記憶體“這個結論了。

  二、靜態方法和非靜態方法的區別?

  在記憶體中的區別是,非靜態方法在建立例項物件時,因為屬性的值對於每個物件都各不相同,因此在new一個例項時,會把這個例項屬性在GC Heap裡拷貝一份,同時這個new出來的物件放在堆疊上,堆疊指標指向了剛才拷貝的那一份例項的記憶體地址上。而靜態方法則不需要,因為靜態方法裡面的靜態欄位,就是儲存在Method Table裡了,只有一份。

  因此靜態方法和非靜態方法,在呼叫速度上,靜態方法速度一定會快點,因為非靜態方法需要例項化,分配記憶體,但靜態方法不用,但是這種速度上差異可以忽略不計。

  三、為什麼要有非靜態方法?

  早期的結構化程式設計,幾乎所有的方法都是“靜態方法”,引入例項化方法概念是物件導向概念出現以後的事情了,區分靜態方法和例項化方法不能單單從效能上去理解,建立c++,java,c#這樣面嚮物件語言的大師引入例項化方法一定不是要解決什麼效能、記憶體的問題,而是為了讓開發更加模式化、物件導向化。這樣說的話,靜態方法和例項化方式的區分是為了解決模式的問題。

  接下來繼續思考,如果我們全部用靜態方法,不用非靜態方法,不是一樣能實現功能嗎?是的,沒錯,但是你的程式碼是基於物件,而不是物件導向的,因為物件導向的繼承和多型,都是非靜態方法。

  第二個原因是為什麼不建議都用靜態方法,我們如果多執行緒的情況下,如果靜態方法使用了一個靜態欄位,這個靜態欄位可以會被多個執行緒修改,因此說如果在靜態方法裡使用了靜態變數,這就會有執行緒安全問題,當然了,就算不是多執行緒,因為靜態欄位只有一份,同樣會有被其他地方修改的問題。

  從這三點我們得出的結論如下:

  一、 什麼時候用靜態方法,什麼時候使用非靜態方法?

  既然靜態方法和例項化方式的區分是為了解決模式的問題,如果我們考慮不需要繼承和多型的時候,就可以使用靜態方法,但就算不考慮繼承和多型,就一概使用靜態方法也不是好的程式設計思想。

  從另一個角度考慮,如果一個方法和他所在類的例項物件無關,那麼它就應該是靜態的,否則就應該是非靜態。因此像工具類,一般都是靜態的。

  二、 為什麼使用單例模式而不用靜態方法?

  從面相物件的角度講:

  雖然都能實現目的,但是他們一個是基於物件,一個是物件導向的,就像我們不面相物件也能解決問題一樣,面相物件的程式碼提供一個更好的程式設計思想。

  如果一個方法和他所在類的例項物件無關,那麼它就應該是靜態的,反之他就應該是非靜態的。如果我們確實應該使用非靜態的方法,但是在建立類時又確實只需要維護一份例項時,就需要用單例模式了。

  比如說我們在系統執行時候,就需要載入一些配置和屬性,這些配置和屬性是一定存在了,又是公共的,同時需要在整個生命週期中都存在,所以只需要一份就行,這個時候如果需要我再需要的時候new一個,再給他分配值,顯然是浪費記憶體並且再賦值沒什麼意義,所以這個時候我們就需要單例模式或靜態方法去維持一份且僅這一份拷貝,但此時這些配置和屬性又是通過物件導向的編碼方式得到的,我們就應該使用單例模式,或者不是物件導向的,但他本身的屬性應該是面對物件的,我們使用靜態方法雖然能同樣解決問題,但是最好的解決方案也應該是使用單例模式。

  從功能上講:單例模式可以控制單例數量;可以進行有意義的派生;對例項的建立有更自由的控制;

  三、其他:

  資料庫連線能不能做SingleTon?

  如果是簡單地把一個connection物件封存在單例物件中,這樣是錯誤的,因此連線池裡有多個連結可以用,如果使用SingleTon,那在WEB訪問時,就只能用一個資料庫連結,那不是死的很慘?

  但是連結池可以使用單例模式,初始化的時候建立譬如100個connection物件,然後再需要的時候提供一個,用過之後返回到pool中,我們用單例模式,是保證連線池有且只有一個。

  再舉個例子,比如DAL層寫好一個呼叫資料庫表的類,在BLL層應用此類時,如果每次都new建立的話需要頻繁的建立和回收,而DAL層這個類裡又沒有和物件相關的值變數,所以不需要每次都new一個,這時候就可以用單例模式來建立這個DAL例項。

相關文章