1. 程式人生 > >類型本質---進階編程篇(二)

類型本質---進階編程篇(二)

自身 string rand 方法 什麽 ldo 一個數 實例化 xpl

  我們在學習一門新的編程語言時,永遠都繞不開變量類型和控制語句,這兩大塊是一個程序的基本構成方式,而且我們也知道構成計算機數據的一切本質其實都是0和1,比如你運行的程序是0和1組成的,你播放的一首歌也是0和1組成的,你看的電影也是0和1組成的,所以一個數據對象肯定也是0和1組成的,一個數據對象沒有類型是沒有辦法想象的,同樣是4個字節的數據,以int,uint,float來看待都是不一樣的結果,也就是如果我們設置的數據類型和讀取的數據類型不一致時,絕大多數情況都會導致意外發生。

  本篇文章將會從兩個不同的方面來解釋,第一個就是數據的本質,作為程序員不得不知道數據本質和類型背後的內容。

數據的本質

  我們已經非常習慣的使用基本的數據類型,比如bool,byte,short,ushort,int,uint,long,ulong,float,double,string等等,這就是常用的大部分類型。比如我們寫了int i=0;那麽請你用二進制來表示這個數據,一般只要學習過計算機原理的人都比較容易就可以寫出0000_0000_0000_0000_0000_0000_0000_0000,因為是32位的數據,所以必須這麽寫,這還是相對好理解的,如果是-1呢,學過計算機的都知道,通常在計算機中,負數采用補碼的形式來存儲,也就是1111_1111_1111_1111_1111_1111_1111_1111,為什麽會是這個數據,一定要搞清楚,因為這個數據+1=0,而且更關鍵的是,這個二進制的數據只有在int下面才表示-1,如果是uint呢,表示多少?就是2^32-1,所以我們可以得出結論,相同的數據在不同的類型下,表示的數據是不一樣的。

  所以說,類型是什麽?類型規定了生成和解析數據的規則,以便我們得到準確的數據,再比如float類型

1 float i = 1;
2 int j = BitConverter.ToInt32(BitConverter.GetBytes(i), 0);

  這兩行代碼就是將float的i的真實數據byte[],用int去解析的話會得到什麽,j=1065353216;這個數據和原來的1真是風馬牛不相及啊。因為整數的存儲機制我們還算比較好理解的,但是計算機只有0和1,想要存小數確實挺困難的,所以采用了一個整數+一個指數的方式來存儲,比如0.1=1*10^(-1),那麽0.1就可以用坐標(1,-1)來標識,想要更深入的了解浮點數的存儲規則,可以查看相關的知識。

  下面來看一個實際的基本應用,在實際的開發中,我們會碰到一些問題,比如數據的簡單存儲,我們需要將數據存儲到本地的一個文件中,一般的C#教程上都只是介紹了txt文件的讀寫,並沒有針對實際開發提出有用的見解。所以此處我們假設我們需要存儲10000個數據,沒有數據都是0-100的整數,剛開始學習編程的時候比較容易想到下面的方法:

 1             //生成一個隨機0-100的10000個數據
 2             int[] data = new int[10000];
 3             Random r = new Random();
 4             for (int i = 0; i < 10000
; i++) 5 { 6 data[i] = r.Next(0, 101); 7 } 8 9 10 System.IO.StreamWriter sw = new System.IO.StreamWriter(@"D:\123.txt", false, Encoding.Default); 11 for (int i = 0; i < 10000; i++) 12 { 13 sw.WriteLine(data[i]); 14 } 15 sw.Dispose();

  讀取數據的時候就反其道而行,一行行的讀取,讀取一行就將字符串轉化成int,這種方式讀取比較慢,而且數據存儲浪費了硬盤空間,我們查看這個文件的大小發現,

技術分享

  還是占了38.1KB(這個是實際的數據,占用空間是指數據消耗掉的容量)的數據,以下是經過改良的版本:

1             System.IO.FileStream fs = new System.IO.FileStream(@"D:\123.txt", System.IO.FileMode.Create);
2             for (int i = 0; i < 10000; i++)
3             {
4                 fs.WriteByte(BitConverter.GetBytes(data[i])[0]);5             }
6             fs.Dispose();

  因為我們的數據都是0-100的,所以我們就存儲一個字節的數據即可,這樣解析的時候更加的快速,文件本身的大小也縮小到了10K,雖然直接打開txt會出現亂碼(因為此處我們寫的數據本來就不是字符串)。10000字節差不多就是10K的樣子,那麽我們還有沒有可能在縮小所存儲的數據呢?答案當然是可以的,此處就使用了一種簡單的壓縮方式,假設數據存儲的順序沒有關系,那我們在存儲的時候,一共也就101種數據,每種數據出現0-10000次,而已,每個數據可以表示成 數據+重復次數來表示,因為重復次數不清楚,所以需要2個字節來表示,那麽每個數據占用3個字節,101*3共占用了303個字節,你看我們就把數據壓縮到了0.3K大小,只是在讀取數據時需要根據存儲規則來反解。

  所以我們在提取重復次數的時候,一般比較容易想到的是這樣的代碼:

 1             //生成一個隨機0-100的10000個數據
 2             int[] data = new int[10000];
 3             Random r = new Random();
 4             for (int i = 0; i < 10000; i++)
 5             {
 6                 data[i] = r.Next(0, 101);
 7             }
 8 
 9 
10             short[] repeat = new short[101];//因為最多重復一萬次而已
11             for (int i = 0; i < 101; i++)
12             {
13                 short count = 0;
14                 for (int j = 0; j < 10000; j++)
15                 {
16                     if (data[j] == i) count++;
17                 }
18                 repeat[i] = count;
19             }

  這麽寫代碼有個問題,如果不是10000個數據呢,而是1000000個呢,那麽計算重復次數的代碼就會非常耗時,我們可以對data進行排序再進行高效的分析,這個就是後話了。同理對於其他的float,double都是一致的效果。

關於兩套類型

  不知道大家在學習C#的時候,會不會碰到這樣的情況,定義一個int時,還有另一種Int32,所以此處列舉所有對應的類型。

 1 sbyte    ====    System.SByte
 2 byte      ====    System.Byte
 3 short     ====    System.Int16
 4 ushort   ====    System.UInt16
 5 int         ====    System.Int32
 6 uint       ====    System.UInt32
 7 long      ====    System.Int64
 8 ulong    ====    System.UInt64
 9 char      ====    System.Char
10 float      ====    System.Single
11 double  ====    System.Double
12 bool      ====    System.Boolean
13 decimal ====    System.Decimal
14 string    ====    System.String
15 object   ====    System.Object
16 dynamic====    System.Object

  使用的效果上,兩個是完全等價的,編譯後的結果也是一致的,給我們的感覺就是這裏有兩套不同的命名方式,有些地方用第一套,有些地方用第二套,相對比較亂,原理是第二套的類型是FCL中原生支持的,也叫基元類型,而第一套類型是C#編譯器提供一個等價的寫法而已,從我們學習C語言的基礎來看,左邊這套命名似乎更加的符合我們的習慣,但是碰到下面的情況又有點尷尬:

1             byte[] data = new byte[4];
2 
3             //寫法一
4             int i = BitConverter.ToInt32(data, 0);
5             float f = BitConverter.ToSingle(data, 0);
6 
7             //寫法二
8             Int32 j= BitConverter.ToInt32(data, 0);
9             Single g = BitConverter.ToSingle(data, 0);

  第一種寫法看上去總有點怪怪的,第二種寫法閱讀起來更加的舒適,實際中具體使用哪個根據自身的情況來選擇,微軟建議是用第一套,但是有些書籍推薦第二套。

類型背後的東西

  上面的一切一切都讓我們以為int i=0;i就真的只是一個4個字節的數據而已,因為我們在使用的過程中從沒有發現其他東西,如果不是自己看書學習,或是從別人那裏得知,就根本不會知道沒有對象還包含了另外兩個數據塊,同步索引塊和類型對象指針,這兩個數據塊構成了整個CLR的基礎,為什麽這麽說呢,首先我們考慮第一個問題:

  如果我寫了個靜態的int變量,就可以在程序的任何地方(可以在不同的線程)進行引用,獲取,設置值而不用擔心其他問題,比如競爭問題。不要思考也還好,一旦要去考慮這個問題的答案,背後就隱藏了一個極大的秘密,對象數據的本質。我們在實例化一個對象後,如下

1 puclic class class1
2 {
3     public static int i=0;  
4 }

  這行代碼不僅僅只是生成了一個4個字節的變量數據,準確的說,對象i的數據部分確實是4個字節而已,但是對象本身絕對不是4個字節的問題,它還有另外兩個非常重要的數據對象,叫同步索引塊和類型對象塊,而其中的同步索引塊就控制了類型在同一瞬間只能進行一次設置,我們知道數據都是01組成,我們在執行i=0xffffff時,在另一個地方剛好獲取i的值,這樣就避免了萬一設置到一半(i=0xff0000),我們就獲取到了錯誤的值的可能性。

  第二個有意思的問題是對象其實知道它自己的類型,這真的是一個很有意思的東西,如果上述的 i 只有4個字節的byte數據,那根本判斷不出來數據類型,現在我們可以調用i.GetType()來獲取i本身的類型,你可能會覺得這玩意到底有什麽用,我自己定義的i我還不知道他是什麽類型嗎?事實上用處大了,我先說明有什麽用處,在說明原因。正是因為對象自己知道自己的類型,才能執行一些類型的轉換,強制轉換也好,隱式轉換也罷,C#所有的轉換建立在這個基礎之上的,再看下面的代碼:

1 int i=0;
2 
3 object obj=(object)i;
4 
5 string m=(string)obj;

  在第二行代碼中,因為編譯器知道object是所有類的基類,所以可以轉化,但是obj對象的類型真的是object嗎?答案是不一定的,因為object是所有類的基類,所以obj理論上來說可以是任何類型,此處你可以獲取類型來確認,obj其實是int類型。正是因為int類型和string類型不存在繼承關系,所以第三行代碼報錯。

  上面也說了另一個數據塊是類型對象指針,說明它會指向一個對象,而這個對象是關於類型的對象,該對象就是在CLR加載程序的時候創建的,我們可以通過類型對象來獲取到更多有用的數據,這部分內容主要涉及到反射技術,將在以後有機會說明。

string類型特點

  string類型有個非常大的特點,字符串是不易變的,所以剛開始寫代碼的時候容易會犯這樣的錯誤(其實也不算錯誤,至少運行仍然可以運行)

1             string str = "";
2             for (int i = 0; i < 10000; i++)
3             {
4                 str += "1";
5             }

  雖然結果上來說,str最終是長達一萬個長度的1組成的,但是這麽寫的效率非常的差,如果你定義了一個字符串string m="123456",它就傻傻的呆在一個內存塊中,不會變化,直到被清除為止,所以上述的代碼需要不停的重新分配和刪除,實際的性能非常差,應該避免這種情況。關於string類型最難的就是本地化了,雖然大多數的程序員都不太關心這個問題,因為大多數的程序都只是給一個特定語言使用的,比如說中文,比如說英文,所以此處就簡單的提個例子,即時兩個看著不同的string,因為語言文化不一致,在比較相同的時候也是可能相同的。

數據重疊問題

  雖然這個技術實際中很少碰到,但是用到的時候就特別合適,它允許數據區域進行重疊,比如和int數據和byte數據,結果就是更改了一個,另一個也會改變,代碼如下:

 1 [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Explicit)]
 2     public class SomeValType
 3     {
 4         [System.Runtime.InteropServices.FieldOffset(0)]
 5         public byte ValueByte = 0;
 6         [System.Runtime.InteropServices.FieldOffset(0)]
 7         public int ValueInt = 0;
 8         [System.Runtime.InteropServices.FieldOffset(0)]
 9         public bool ValueBool = false;
10     } 

  也可以自己寫寫代碼,測試測試,還是相當有意思的。

類型本質---進階編程篇(二)