1:  protected void Page_Load(object sender, EventArgs e)
   2:  {
   3:      double x = Math.Round(2.5);
   4:      double y = Math.Round(3.5);
   5:      Response.Write(
   6:          string.Format("<li>x={0} y={1}",
   7:          x, y)
   8:      );
   9:      float f = 5000001;
  10:      float g = 5000002;
  11:      float h = f + g;
  12:      Response.Write(
  13:          string.Format("<li>{0:#,#} + {1:#,#} = {2:#,#}",
  14:          f, g, h)
  15:      );
  16:  }

今天來談談.NET中的數學,以上的程式碼裡有兩題算數,請用眼睛Build & Run,再告訴我答案。

真簡單,答案分別是x=3 y=4、5,000,001 + 5,000,002 = 10,000,003,問這種小學生都會的問題,似乎有污辱大家智慧的嫌疑。

事情當然沒這麼簡單,如果你真的跑一下上述的Code,會懷疑電腦是不是瘋了...

  • x=2 y=4
  • 5,000,001 + 5,000,002 = 10,000,000

    這裡有兩個陷阱:

    1) 大家看到Math.Round,應該會認為它跟Javascript中的Math.round,T-SQL/PL-SQL中的Round函數一樣,就是四捨"五"入唄。逄5要進位連小學生都知道,但為什麼Math.Round(2.5)=2? 仔細看一下Math.Round的函數說明,你會看到這一句話: 最接近參數 d 的整數。如果 d 正好為兩個整數的中間數 (一個為偶數,另一個為奇數),則會傳回偶數。這是所謂的銀行家捨入法(Round-To-Even,也叫做Bankers' Rounding,洋人銀行家不知是否也像中國人一樣,講究好事成雙,迷信起雙數才吉利這一套,四捨六入五成雙的規則可以縮小先捨入再累加的計算誤差,例如:1.5+2.5=4,若用四捨五入2+3=5,而四捨六入五成雙則是2+2=4,更接近原值。[感謝網友楊樹人補充] 事實上VB、VBScript以來就是依循這個準則做捨入。) 如果希望.NET用我們從小熟悉的法則做四捨五入,可以加上MidpointRounding.AwayFromZero參數,例如: Math.Round(2.5, MidpointRounding.AwayFromZero)就會得到3。

    2) 第二個陷阱是關於float,依照文件的說明,它的精確度只有七位! 也就是說當你在float變數中塞入八位以上的數字,系統不會出錯,但第七位之後的數字會悄悄地蒸發掉。理論上一千萬只差個位數應該影響有限,但做過帳務系統的人都知道"差一元"的威力,它常常讓一群人七晚八晚回不了家,神情慌亂,披頭散髮,滿身大汗,狼狽不堪,上窮碧落下黃泉,只為了找出少掉的一塊錢藏在哪裡... 總之,如果你的程式要運算鉅額的金錢數字,最好乖乖用double(精確度15-16位)或乾脆用decimal(28-29位)算了。如果要做到一分一毫都不容有誤,decimal才是王道,勿用浮點(float、double),請記口訣:「算數用浮點,遲早被人扁」。


  • Comments

    # by wayne

    Thanks, very helpful!

    Post a comment