[小菜一碟] C# 的 double 在 SQL Server 應該要存成 float,搞清楚單精度跟雙精度的差別。

SQL Server 的資料型別中有一個 float,這個 float 在 C# 對應的應該是 double,而 C# 的 float 對應的 SQL Server 資料型別應該是 real,那如果將 real 對應成 C# 的 double 會發生什麼事? 答案是小數點的精準度會跑掉。

以往我們認知上有效範圍較小的資料型別,可以正確地被有效範圍較大的資料型別表示,例如:int 的 MaxValue 指定給 long 型態的變數是完全沒問題的,但是在有小數點的世界卻不見得是這樣,如下圖我將 float 的值強制轉型成 double 就變得怪怪的,反過來就正常。

在 C# 中 float 佔 4 bytes,double 佔 8 bytes,那為什麼 double 無法精準地表示 float 的值? 因為 float 跟 double 最主要的差別不在於數值的有效範圍,而是數值的精準度,電腦是二進制的世界,我們用二進制可以精準地表達任何的正負整數,但是卻無法精準地表達任何小數,例如 0.1。

單精度的二進制表達方式是這樣的(雙精度亦同),它分三個區塊:

  • Sign:符號,佔 1 bit,0 為正數,1 為負數。
  • Exponent:指數,單精度佔 8 bits,127 代表 0,雙精度佔 11 bits,1023 代表 0。
  • Fraction:分數,就是有效位數,單精度佔 23 bits,雙精度佔 52 bits。

例如,我要用單精度表達 -15.625 的二進制是這樣算的,先不管正負符號,把 15.625 轉成二進制 1111.101,再抽出指數 1.111101 × 2^3,我就可以得到:

  • Sign = 1
  • Exponent = 127 + 3 = 10000010
  • Fraction = 11110100000000000000000(不足位數補 0)

最終 -15.625 的單精度二進位值就等於 1 10000010 11110100000000000000000,但是我們卻無法用二進制來完整表達 0.1,它會一個無窮位數的二進位值,所以到底要精準到二進位值的哪一個位數,因此就有了單精度跟雙精度的差別。

這就好比秤,同樣可以秤 1 公斤的秤,一個顯示的是公克,一個顯示的是毫克,把 300 公克的麵粉放到這兩個秤上,一個會顯示 300 公克,一個可能會顯示 299999 或 300001 毫克,所以 0.1 這個數值用單精度跟雙精度來表達,精準度就不一樣,可以看到顯示的二進位值差很多。

  • 單精度:0 01111011 10011001100110011001101
  • 雙精度:0 01111111011 1001100110011001100110011001100110011001100110011010

如果硬要把單精度的數值用雙精度來呈現,不足的二進位值就會補 0,就產生誤差了,這就是為什麼 (double)0.1f 會是 0.100000001490116 這樣奇怪的數字。

所以我們平常在寫程式的時候,應該儘量避免將單精度數值跟雙精度數值互相轉換,以免產生誤差,也不會造成處理上的麻煩,個人認為在開發一般的商業應用程式,應該在事前多做分析,來確定數值的有效小數位數,使用 decimal 來加以限制,而不是偷懶直接用 float 或是 double。

參考資料

C# 指南 ASP.NET 教學 ASP.NET MVC 指引
Azure SQL Database 教學 SQL Server 教學 Xamarin.Forms 教學