C#的實數型別有三種:float、double、decimal。其中 float、double 為浮點數,本站的老讀者們一定知道-「算錢用浮點,遲早被人扁」的道理,因此只要涉及金額計算,我一律改用 decimal 型別。前幾天,踩到 decimal 小數尾數零地雷一枚。
以下程式為例,大家猜猜結果為何?
class Program
{
staticvoid Main(string[] args)
{
float flt = 1.2300F;
Console.WriteLine(flt);
double dbl = 1.2300D;
Console.WriteLine(dbl);
decimal dcm = 1.2300M;
decimal cmp = 1.23M;
Console.WriteLine(dcm == cmp);
Console.WriteLine($"{dcm} vs {cmp}");
Console.Read();
}
}
註:實數常值(Real Literal)後方需加上尾碼以明確宣告型別,F f 代表 float、D d 或不加尾碼代表 double、M m 則代表 decimal。
由結果可知,浮點數型別(float、double)不會保留小數尾數零,decimal 則可做到 1.23 與 1.2300 有別:進行 = 比對二者相等,但 ToString() 時小數尾數零可忠實還原。
事情發生在一段分別由 SQL 及 Oracle 取出對應資料比對的程式,目的在驗證兩邊資料庫是否一致。為簡化比對邏輯,我將所有欄位內容 ToString() 轉為字串,心想 SQL 與 Oracle 的 Schema 一致,欄位都是 DECIMAL(5,4) 且儲存數值相同,豈有寫入 decimal 後 ToString() 結果不同的道理?結果,真的被我遇上了。
用以下範例重現問題,在 SQL 與 Oracle 端分別將 1.23 存入 DECIMAL(5,4),再用 Dapper 讀取到 decimal:
staticvoid Main(string[] args)
{
using (var cn = new SqlConnection(csSql))
{
decimal d = cn.Query<decimal>(
@"
declare @d decimal(5,4);
set @d=1.23;
select @d;").Single();
Console.WriteLine(d);
}
using (var cn = new OracleConnection(csOra))
{
cn.Open();
cn.Execute(
@"create global temporary table decimal_test (d decimal(5,4)) on commit preserve rows");
cn.Execute("insert into decimal_test values (1.23)");
decimal d = cn.Query<decimal>("select d from decimal_test").First();
Console.WriteLine(d);
cn.Execute("truncate table decimal_test");
cn.Execute("drop table decimal_test");
}
Console.Read();
}
結果分別為 1.2300 與 1.23。SQL Client 讀取 DECIMAL(5,4) 轉入 decimal 時後方會補足 0 到精確位數,而 ODP.NET 不會,二者的行為差異造成讀取 decimal 的小數尾數零數目不同,不影響大於小於等於比對,遇到 ToString() 轉字串,便會得到不同結果。
至於要怎麼去除 decimal ToString() 夾帶的尾數零,有幾種做法:
- 如果確定要保留小數位數上限,可以寫成 ToString("#.####")。缺點是若 # 個數小於實際小數位數會被四捨五入影響精確度。若求保險,小數點後寫上 28 個 # 肯定安全。(decimal 精確度上限為 29 位)
- 用 ToString("G29") 轉為科學記號,但要求 29 位精準位數,成為位數足又不會出現 E 的幾次方的偽科學計號。缺點是 0.00001 這類微小數會被轉成 1E-05。
- 在 Stackoverflow 看到奇妙解法,decimal 除上 1.00000…(29個0):
publicstaticdecimal Normalize(thisdecimalvalue)
{
returnvalue/1.000000000000000000000000000000000m;
}
寫成擴充方法,1.2300m.Normalize() 尾數零就會清光光。
簡要心得
- decimal 會保存小數尾數零,float、double 等浮點型別不會
- 小數尾數零不影響大於等於小於比對,但會影響 ToString() 結果
- SQL Client 與 ODP.NET 讀取 DECIMAL(m, n) 欄位寫入 decimal 時對於小數尾數零的處理原則不同
- 去除 decimal 小數尾數零有幾種做法:ToString("#.####")、ToString("G29") 以及奇妙的 Normalize() 方法
- 政令宣導時間:「算錢用浮點,遲早被人扁」。早晚複誦,永誌不忘~