實值型別、參考型別和泛型多語言
咱們先不說主題,先說說CLR支援多語言。 .net有個非常強大的特點,那就是跨語言,支援很多語言,比如C#、J#等。先來個圖看一看
C# J# VB 等等等 C#編譯器 J#編譯器 VB編譯器 編譯器 --------------------------------- | 中繼語言 | --------------------------------- 編譯(運行) 機器語言
看到這個圖,每個語言都有自己的編譯器,通過第一次編譯,編譯成中間檔案(dll或是exe檔案)。在程式啟動並執行時候,再次編譯把中間檔案編譯成機器語言。
但是,CLR支援這麼多語言不會出為題嗎?換句話說是為什麼CLR會支援這麼多語言?
就像一個四川人和一個河南人對話,互相的聽不懂,但是,他們兩個人都說普通話就能交流了。在CLR中,每種語言就相當於各個地方的人們,相互交流困難,但是有了普通話這個規範(CTS(公用語言類型)和CLS(Common Language Specification)),就可以說話了。CTS相當於我們普通話的音節,而CLS相當於普通話的約定的文法。這樣子呢,就組成了.net大家庭。在CTS中,就是實值型別和參考型別
進程 線程 記憶體空間
本來想單獨整理一下進程和線程的,後來想和堆棧一起簡單的說更好
在一個應用程式中,會分配一個進程和最大4G的記憶體空間,這個記憶體空間會分為4個區,全域資料區、代碼區、線程堆棧區、託管堆區。
(線程是程式啟動並執行最小單位,線程之間是隔離的,在Window下,每個程式都會有主進程,分配的內容最大為4G)
整個項目都是在這個進程啟動並執行。在這個進程中,可能有很多線程,每一個線程都會在棧下分配一個1M的託管堆棧的空間。
全域資料區:存放著全域變數、待用資料、常量代碼區:存放所有的程式碼線程堆棧區:存放為運行而分配的局部變數、參數、返回資料、返回地址託管堆區:自由儲存區--------------------------------------------------------------------------------| ---------------------------------------- -------------------- || | ---- --- --- ---- | | | || | |線| |線| |線| |線| | | | || | |程| |程| |程| |程| | | | || | |1 | |2| |3| | 3| | | 托 管 堆 | || | ---- --- --- ---- | | | || | (線程數量取決於你的代碼) | | | || | 線 程 堆 棧 | | | | | ---------------------------------------- -------------------- || || || ------------------------------- ----------------------------------- || | | | | || | 全 局 數 據 區 | | 代 碼 區 | || | | | | || | | | | || ------------------------------- ----------------------------------- |--------------------------------------------------------------------------------
實值型別與參考型別
C#中,一般情況下實值型別存在它申明的地方,參考型別存在託管堆上。實值型別轉換參考型別叫裝箱,參考型別轉換實值型別叫拆箱
實值型別
對於實值型別的執行個體,CLR在運行時有兩種分配方式:(1) 如果該實值型別的執行個體作為類型中的方法(Method)中的局部變數,則該執行個體被建立線上程棧上;(2) 如果該實值型別的執行個體作為類型的成員,則該執行個體作為參考型別(參考型別在GC堆或者LOH上建立)的執行個體的一部分,被建立在GC堆上
參考型別
對於參考型別的執行個體,CLR在運行時也有兩種分配方式:(1) 如果該參考型別的執行個體的Size<85000Byte,則該執行個體被建立在GC(Garbage Collection)堆上(當CLR在分配和回收對象時,GC可能會對GC堆進行壓縮);(2) 如果該參考型別的執行個體的Size>=85000byte,則該執行個體被建立在LOH(Large Object Heap)上(LOH不會被壓縮)。
說到了這裡,鋪墊也Ok了,下面有一個題: 在不考慮多線程的條件下,定義了個結構StructObj,裡面有int類型的變數x;又定義了一個類ClassObj,裡面也有int類型的變數x。
ClassObj r1 = new ClassObj();//在堆上分配 StructObj v1 = new StructObj();//在棧上分配 r1.x = 5;//根據地址找到參考型別,進行修改 v1.x = 5;//在棧上修改 ClassObj r2 = r1;//只複製引用 StructObj v2 = v1;//在棧上分配空間並複製成員 v2.x = 6; r2.x = 6; Console.WriteLine("v2.x=" + v2.x); Console.WriteLine("r2.x=" + r2.x); Console.WriteLine("v1.x=" + v1.x); Console.WriteLine("r1.x=" + r1.x); Console.WriteLine(r1.Equals(r2).ToString()); Console.WriteLine(v1.Equals(v2).ToString());
現在的列印結果是什嗎? 答案是:
v2.x=6r2.x=6v1.x=5r1.x=6TrueFalse
來個圖理解下
線程堆棧 託管堆 r1-------| v1.x=5 |------R=6(r2重新賦值前這個值為5) r2-------| v2.x=6
畫的比較抽象,r1是參考型別,r1的值存在聲明他的地方,在棧上存的是託管堆的地址,賦值r1.x=5,v1.x=5 再次執行個體化了,StructObj被儲存在了他聲明的地方,又在棧上開闢了一個空間儲存x=6,r2執行個體化只是在複製了引用,再次賦值的時候會覆蓋原先的5,那麼r1.x和r2.x值就會相等,說的明白點就是r1和r2就是一個東西。
那麼r1就會和r2完全相等(值,地址等等) 比較實值型別只會比較他們的值是否相等,所以為False
要是還是看不懂,這還能怪我咯(有本事你來打我)
突然想到一個有意思的問題:為什麼要分實值型別個參考型別?
早在C#出現之前就已經存在了實值型別和參考型別。著麼說呢,也應該是一種無奈把。在Window下的應用程式,一個應用程式下假設有四個線程,每一個線程都會分配一個1M的空間,那麼4個線程就是4M的空間。先來了一個1.2M的包存進去了,又來了一個1.2的包存進了,現在來了個3M的包發現不足了,這 是他不會開闢新的空間,可能釋放了一個1.2包還存不進去。這是就有記憶體片段產生了。如果把所以的資料都當實值型別用,線程堆棧區很快就會滿了而且丟出很多記憶體片段拖慢伺服器速度。那麼我將大的資料存在託管堆上,線上程堆棧上儲存一個我在託管堆存放資料的地址,而實值型別是一些很小的資料,並且是線程啟動並執行時候才能使用的,所以放在了線程堆棧上。而參考型別裡面可能有很多東西,資料比較大,就存在了託管堆上
現在再來談裝箱和拆箱
裝修是將實值型別轉換成object,再將封裝後的Object Storage Service在堆上的一個過程。而拆箱是從object到實值型別的轉換,先檢查對象,確保他是給定實值型別的一個裝箱值後。將值複製到新的實值型別中,釋放當前對象 由此看來,不管是裝箱還是拆箱,都需要CPU大量的計算
數組、ArrayList、泛型
數組被大部分語言支援,其優點在於是連續儲存的,所以他的索引速度是非常的快,而且賦值與修改元素也很簡單,訪問速度較快。缺點是數組是定長的,在建立的時候,就需要知道其大小。而且插入資料和刪除很費勁。 相對於數組來說,ArrayList解決了資料的問題,可以很靈活的插入資料和刪除。由於ArrayList對資料類型並不嚴格要求,在添加的時候使用object類型來容納添加的對象,這裡會有裝箱拆箱的操作,照成了效能的損失。
為瞭解決這種問題,微軟推出了泛型集合List,如下
List<int> list=new List<int>();
可以看到,在定義泛型集合的時候已經指定了類型。
因為泛型指定了類型,所以在存取的時候值限制於限定的類型內。這樣就避免看裝箱拆箱所消耗的效能問題,又同時提高了安全性
然後再說說泛型,在這裡需要特別說明的是,泛型不能直接的說是實值型別還是參考型別,而是指定類型.
在工作總,常常會遇到泛型的代碼,我在操作的時候不需要在乎你是什麼類型,你給我傳過來是什麼類型我就處理什麼類型,或是你需要什麼類型我給你什麼類型。比如:
public class Www<T> where T: ClassObjP{ public Www() { }}
在這裡,T表示任意類型,但是,T只是個符號,不是關鍵字,寫A、B、Z等等等等都可以。ClassObjP表示是約定的類,約定製定類的範圍(這類以及這個類的兒子),遵循曆史替換原則的類(原諒自己表達能力)
好了,總結的夠多了。現在最後在拓展一下
協變 逆變
上面剛剛在約束裡過了一下,也提了下曆史替換原則,而在工作中,我們又很小的可能會遇到一些因為沒有遵循裡氏替換原則的代碼而報錯,這裡就可能用得到協變逆變
從使用上來說:協變是子類轉父類,只能用在輸出參數out;逆變是父類轉子類,只能用在輸入參數
在用法上呢,可以使用協變,可以使用逆變,也可以協變和逆變輔助使用