Hello World
Spiga

关于浮点数计算时的精度问题

2009-11-24 14:21 by 老赵, 10079 visits

那个有问题的缩略图生成的方法发布之后,短短半天就有很多朋友响应,其中指出了不少方法中的不少问题,有些也是我没有意识到的。果然集体的智慧是无穷的,一段代码在许多人的眼皮底下经过,想留有bug也不容易。不过,我在这里只能谈一下我写那篇文章的本意了,我认为那篇文章中最主要的问题是,在计算图片尺寸时没有处理好浮点数计算的精度问题。

为了凸现主要逻辑,我把之前那个方法中计算图片尺寸的代码单独抽取成一个方法:

public static void GetThumbnailSize(
    int originalWidth, int originalHeight,
    int desiredWidth, int desiredHeight,
    out int newWidth, out int newHeight)
{
    // If the image is smaller than a thumbnail just return it
    if (originalWidth <= desiredWidth && originalHeight <= desiredHeight)
    {
        newWidth = originalWidth;
        newHeight = originalHeight;
        return;
    }

    // scale down the smaller dimension
    if ((decimal)desiredWidth / originalWidth < (decimal)desiredHeight / originalHeight)
    {
        decimal desiredRatio = (decimal)desiredWidth / originalWidth;
        newWidth = desiredWidth;
        newHeight = (int)(originalHeight * desiredRatio);
    }
    else
    {
        decimal desiredRatio = (decimal)desiredHeight / originalHeight;
        newHeight = desiredHeight;
        newWidth = (int)(originalWidth * desiredRatio);
    }
}

我们通过简单的代码试验一下:

int newWidth, newHeight;

GetThumbnailSize(200, 200, 100, 100, out newWidth, out newHeight);
Console.WriteLine("{0}, {1}", newWidth, newHeight);

GetThumbnailSize(300, 300, 100, 100, out newWidth, out newHeight);
Console.WriteLine("{0}, {1}", newWidth, newHeight);

得到的结果是:

100, 100
99, 100

第一个结果自然没有问题,但是在第二个结果中为什么是99而不是100?为此,我们再通过以下的代码来观察一番:

ratio: 0.3333333333333333333333333333
new value: 99.99999999999999999999999999
to int: 99

可见,虽然使用了decimal,精度已经非常高的,但是在经过了一除一乘,它还是没有恢复到最精确值。虽然一直说要注意浮点数计算时的精度问题,但是对于这个问题许多朋友往往只是理解到“不能直接两个浮点数相等”,包括我自己的第一印象。但事实上,从上面的结果也可以看出,把一个浮点数直接转换成整形,它便是使用了“去尾”而不是“四舍五入”的方法。因此,虽然newValue的值无比接近100,但是在强制去尾后它还是变成了99。

如果要在原来的方法中改变这个问题,最简单的方法可能是把最后的强制转型替换成Math.Round方法。Math.Round方法使用四舍五入,应该能够解决问题。不过如果只是这样的话收获不大,我们再仔细想想,应该如何做到尽可能的精确。

两个浮点数相除可能会丧失精度,但如果是乘法操作,在一般情况下精度是不会丢失的,除非发生了溢出的话,或者小数位数太多。因此在计算过程中为了保持精度,我们应该尽可能的做乘法,而不是作除法。例如以下的判断:

if ((decimal)desiredWidth / originalWidth < (decimal)desiredHeight / originalHeight)

其实最好改写成“等价”的乘法操作(假设没有溢出):

if (desiredWidth * originalHeight < desiredHeight * originalWidth)

同理,如果可以的话,在作计算的时候,也最好先乘再除:

if (desiredWidth * originalHeight < desiredHeight * originalWidth)
{
    newWidth = desiredWidth;
    newHeight = (int)Math.Round((decimal)originalHeight * desiredWidth / originalWidth);
}
else
{
    newHeight = desiredHeight;
    newWidth = (int)Math.Round((decimal)originalWidth * desiredHeight / originalHeight);
}

这么做,我们就避免了使用scaleRatio这个已经丧失部分精度的值来参与计算,这样1 * 3 / 3便可以等于1,而不像1 / 3 * 3等于0.99…。因此,最终我们CreateThumbnail的代码便修改为:

/// <summary>
/// Creates a thumbnail from an existing image. Sets the biggest dimension of the
/// thumbnail to either desiredWidth or Height and scales the other dimension down
/// to preserve the aspect ratio
/// </summary>
/// <param name="imageStream">stream to create thumbnail for</param>
/// <param name="desiredWidth">maximum desired width of thumbnail</param>
/// <param name="desiredHeight">maximum desired height of thumbnail</param>
/// <returns>Bitmap thumbnail</returns>
public Bitmap CreateThumbnail(Bitmap originalBmp, int desiredWidth, int desiredHeight)
{
    // If the image is smaller than a thumbnail just return it
    if (originalBmp.Width <= desiredWidth && originalBmp.Height <= desiredHeight)
    {
        return originalBmp;
    }

    int newWidth, newHeight;

    // scale down the smaller dimension
    if (desiredWidth * originalBmp.Height < desiredHeight * originalBmp.Width)
    {
        newWidth = desiredWidth;
        newHeight = (int)Math.Round((decimal)originalBmp.Height * desiredWidth / originalBmp.Width);
    }
    else
    {
        newHeight = desiredHeight;
        newWidth = (int)Math.Round((decimal)originalBmp.Width * desiredHeight / originalBmp.Height);
    }

    // This code creates cleaner (though bigger) thumbnails and properly
    // and handles GIF files better by generating a white background for
    // transparent images (as opposed to black)
    // This is preferred to calling Bitmap.GetThumbnailImage()
    Bitmap bmpOut = new Bitmap(newWidth, newHeight);
    
    using (Graphics graphics = Graphics.FromImage(bmpOut))
    {
        graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
        graphics.FillRectangle(Brushes.White, 0, 0, newWidth, newHeight);
        graphics.DrawImage(originalBmp, 0, 0, newWidth, newHeight);
    }

    return bmpOut;
}

当然,在前文中很多朋友指出的其他一些问题也很有道理,例如:

  • 没有做参数校验。
  • 直接返回源图片的做法让方法的含义不同。
  • 经过计算后newWidth和newHeight可能为0。

例如还有朋友提出对GIF的处理不很妥当等等——如果您有其他想法的话,也可以继续讨论。或者,你也来分享一下代码或工作中发现的问题?

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

70 条回复

  1. 随碟附送的
    *.*.*.*
    链接

    随碟附送的 2009-11-24 14:25:00

    先抢个沙发,坐下看一下。

  2. OctoberOne
    *.*.*.*
    链接

    OctoberOne 2009-11-24 14:27:00

    good

  3. 钧梓昊逑
    *.*.*.*
    链接

    钧梓昊逑 2009-11-24 14:27:00

  4. JimLiu
    *.*.*.*
    链接

    JimLiu 2009-11-24 14:32:00

    话说我从C语言开始学起,我习惯性地会
    #define epsilon 0.000001
    然后float f;
    int a = (int)(f+epsilon);
    很龊的办法。
    要减少精度烦恼还是尽量少做除法为妙啊。

  5. 温景良(Jason)
    *.*.*.*
    链接

    温景良(Jason) 2009-11-24 14:34:00

    哎,该死的小数点精度害死我了,系统在设计的时候没考虑那么多,结果算到最后总是差1或者2块,晕死了

  6. 苏飞
    *.*.*.*
    链接

    苏飞 2009-11-24 14:36:00

    看了你的文章,让我想起了当时做我们公司新产品时的一个问题,就是把*写在了后台,/写在了前面,最后在给用户返利之时,一直无法对上帐,总是差几分,差几毛的,怎么查也不查不到什么原因,调试发现/过后值不对,但没有想到问题是这样的,最后我是用了Math.Round问题算是解决了,一直没有想明白是那里的错,原来问题出在这里,看过文章我又写了段程序试了试,还真的是这样,有点高兴来留个脚印,呵呵

  7. 那是因为你没有做过财务系统[未注册用户]
    *.*.*.*
    链接

    那是因为你没有做过财务系统[未注册用户] 2009-11-24 14:54:00

    温景良(Jason):哎,该死的小数点精度害死我了,系统在设计的时候没考虑那么多,结果算到最后总是差1或者2块,晕死了


    所以你很少关注小数点的问题。

  8. 苏飞
    *.*.*.*
    链接

    苏飞 2009-11-24 14:55:00

    @那是因为你没有做过财务系统
    关于钱的问题以后都要小心,不管怎么样一定要记录详细,就算是失败了也能证据可查

  9. 一分都不能差![未注册用户]
    *.*.*.*
    链接

    一分都不能差![未注册用户] 2009-11-24 15:03:00

    苏飞:看了你的文章,让我想起了当时做我们公司新产品时的一个问题,就是把*写在了后台,/写在了前面,最后在给用户返利之时,一直无法对上帐,总是差几分,差几毛的,怎么查也不查不到什么原因,调试发现/过后值不对,但没有想到问题是这样的,最后我是用了Math.Round问题算是解决了,一直没有想明白是那里的错,原来问题出在这里,看过文章我又写了段程序试了试,还真的是这样,有点高兴来留个脚印,呵呵


    对于涉及到钱的软件,肯定要做到一分都不能差!

    象超市那样将分全部舍去,但账目还是计算到分的。

  10. zengxiangzhan
    *.*.*.*
    链接

    zengxiangzhan 2009-11-24 15:06:00

    double heightRatio = (double)picture.Height / picture.Width;
            double widthRatio = (double)picture.Width / picture.Height;
    
            int desiredHeight = imageSize.Height;
            int desiredWidth = imageSize.Width;
    
            imageSize.Height = desiredHeight;
            if (widthRatio > 0)
                imageSize.Width = Convert.ToInt32(imageSize.Height * widthRatio);
    
            if (imageSize.Width > desiredWidth)
            {
                imageSize.Width = desiredWidth;
                imageSize.Height = Convert.ToInt32(imageSize.Width * heightRatio);
            }
    
    

    像这种又如何呢?发表一下各位的意见

  11. 不是小心[未注册用户]
    *.*.*.*
    链接

    不是小心[未注册用户] 2009-11-24 15:06:00

    苏飞:
    @那是因为你没有做过财务系统
    关于钱的问题以后都要小心,不管怎么样一定要记录详细,就算是失败了也能证据可查


    因为财务有严格的对账系统,所以即使差一分钱也无法结账。

  12. KKKK[未注册用户]
    *.*.*.*
    链接

    KKKK[未注册用户] 2009-11-24 15:08:00

    换成 乘法 本身好像没解决问题吧。
    最后还是通过 Math.Round来解决的。

    在这个场景下,除法是不可避免的。

  13. 老赵
    admin
    链接

    老赵 2009-11-24 15:10:00

    @KKKK
    解决一部分问题:
    1 / 3 * 3 = 0.99
    1 * 3 / 3 = 1

  14. 只要有心就好解决[未注册用户]
    *.*.*.*
    链接

    只要有心就好解决[未注册用户] 2009-11-24 15:15:00

    象医院进药都是大包装,而病人在药房拿药都是小包装,甚至住院的病人还可以拆开包装用药。这些都牵扯到小数的问题。说简单也简单,说复杂也复杂,只要用心就好解决。

    所以说这不是一个技术问题。

  15. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-11-24 15:27:00

    其实我记得这个问题我以前要天天说,时时叨。。。浮点型不能用于精确(无损)计算。。。。。



    其实在这个场景中,可以不需要使用浮点型。

    公式其实是这样推导出来的:
    x / x' = y / y'
    x = y * x' / y'

    为了使得计算的行为完全可控,我们可以手动整数除法四舍五入。

    x = (y * x' * 10 + 5) / y' / 10;

    这样纯整型运算的好处在于精度是在我们控制范围内的。

  16. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-11-24 15:35:00

    因为很多人脑子里对于浮点型和整型的理解就是小数和整数的区别,这是非常错误的!

    浮点型和整型的差别在于近似计算和精确(无损)计算,浮点型只能用于近似计算,其计算结果无精确保证。

    很难以置信还有这么多人将浮点数用于财务运算。当然这与MSDN的误导也不无关系,decimal适合于财务运算。。。。这是非常扯淡的,任何浮点数无论其精度多高,采用十进制还是二进制,在本质上就不能用于精确(无损)计算。

  17. JimLiu
    *.*.*.*
    链接

    JimLiu 2009-11-24 16:06:00

    @Ivony...
    其实我想有理数还是可以表示的,毕竟有理数一定能表示成分数,分数又是由两个整数表示的,整数就能用二进制精确表示了。

    不过要真弄到这份上,也就不知道到底啥程序用得着了。

  18. chenkai
    *.*.*.*
    链接

    chenkai 2009-11-24 16:07:00

    @Ivony...
    有理 确实是 关注地方太多 分散了精力 类似这些细枝末节的地方 确实很难从设计层面上就能考虑的非常清楚....

  19. 我也来说说浮点数[未注册用户]
    *.*.*.*
    链接

    我也来说说浮点数[未注册用户] 2009-11-24 16:53:00

    早在DOS时代只有浮点类型,而没有什么十进制类型。那时处理小数问题非常的麻烦,每次处理浮点数时都要将其转为字符类型,并按小数位进行截断,然后再次转为浮点类型,以消除小数位后的数值。

  20. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-11-24 16:55:00

    我也来说说浮点数:早在DOS时代只有浮点类型,而没有什么十进制类型。那时处理小数问题非常的麻烦,每次处理浮点数时都要将其转为字符类型,并按小数位进行截断,然后再次转为浮点类型,以消除小数位后的数值。




    这个与什么DOS时代没有什么关系吧,decimal并不是一个原生类型,在C里面我们也完全可以写一个struct decimal。只不过C不能重载运算符罢了。

  21. 用C?[未注册用户]
    *.*.*.*
    链接

    用C?[未注册用户] 2009-11-24 17:01:00

    Ivony...:
    这个与什么DOS时代没有什么关系吧,decimal并不是一个原生类型,在C里面我们也完全可以写一个struct decimal。只不过C不能重载运算符罢了。


    那谁又能用C来做一个数据类的管理软件呢?

  22. winter-cn
    *.*.*.*
    链接

    winter-cn 2009-11-24 17:05:00

    用C?:

    Ivony...:
    这个与什么DOS时代没有什么关系吧,decimal并不是一个原生类型,在C里面我们也完全可以写一个struct decimal。只不过C不能重载运算符罢了。


    那谁又能用C来做一个数据类的管理软件呢?


    又不是没人做过 这跟语言有什么关系 难道你以为C只能写黑屏输出数据么?

  23. winter-cn
    *.*.*.*
    链接

    winter-cn 2009-11-24 17:08:00

    JimLiu:
    @Ivony...
    其实我想有理数还是可以表示的,毕竟有理数一定能表示成分数,分数又是由两个整数表示的,整数就能用二进制精确表示了。

    不过要真弄到这份上,也就不知道到底啥程序用得着了。


    所以说终极解决方案就是写一个分数类型 直到最后一次运算结束才把它转换成浮点类型

  24. 你说的也有道理[未注册用户]
    *.*.*.*
    链接

    你说的也有道理[未注册用户] 2009-11-24 17:10:00

    winter-cn:
    又不是没人做过 这跟语言有什么关系 难道你以为C只能写黑屏输出数据么?


    天下没有绝对的事情。

  25. 老赵
    admin
    链接

    老赵 2009-11-24 17:18:00

    winter-cn:
    又不是没人做过 这跟语言有什么关系 难道你以为C只能写黑屏输出数据么?


    话说……跟着潭老学C语言的很可能有这想法的……

  26. 他的稿费赚大发了[未注册用户]
    *.*.*.*
    链接

    他的稿费赚大发了[未注册用户] 2009-11-24 17:25:00

    Jeffrey Zhao:
    话说……跟着潭老学C语言的很可能有这想法的……


    谭老估计不会计较稿费的小数位!

  27. winter-cn
    *.*.*.*
    链接

    winter-cn 2009-11-24 17:25:00

    @Jeffrey Zhao
    嗯 谭老很好 不过如果没有谭老 那些人也许根本不知道C语言是啥
    谭老真可谓是著作等身了 他以前来我们学校演讲 他出的书堆起来比他高多了 虽然质量差点 而且copy paste居多......
    不管怎么说,也算是成就了

  28. 什么叫终极方案?[未注册用户]
    *.*.*.*
    链接

    什么叫终极方案?[未注册用户] 2009-11-24 17:50:00

    winter-cn:
    所以说终极解决方案就是写一个分数类型 直到最后一次运算结束才把它转换成浮点类型


    这是内行人说了外行话!

  29. winter-cn
    *.*.*.*
    链接

    winter-cn 2009-11-24 18:03:00

    @什么叫终极方案?
    终极解决方案,指能够解决问题的根本原因的方案。

  30. winter-cn
    *.*.*.*
    链接

    winter-cn 2009-11-24 18:03:00

    @什么叫终极方案?
    Final solution

  31. 明白你的终极方案了[未注册用户]
    *.*.*.*
    链接

    明白你的终极方案了[未注册用户] 2009-11-24 18:13:00

    winter-cn:
    @什么叫终极方案?
    终极解决方案,指能够解决问题的根本原因的方案。


    刚才老婆拿来一只烧鸡,又开了一瓶酒,我的终极方案就是张嘴......

    你还年青,所以想问题太“终极”......

  32. qiaojie
    *.*.*.*
    链接

    qiaojie 2009-11-24 18:13:00

    事实上这个问题不需要用浮点数来计算的,用整数就可以了,给一个我的参考实现,加上了最小尺寸限制和四舍五入,应该是比较完整了。

    public static void GetThumbnailSize(
        int originalWidth, int originalHeight,
        int desiredWidth, int desiredHeight,
        out int newWidth, out int newHeight)
    {
        desiredWidth = Math.Min(Math.Max(1, originalWidth), desiredWidth);
        desiredHeight = Math.Min(Math.Max(1, originalHeight), desiredHeight);
    
        if (desiredWidth * originalHeight < originalWidth * desiredHeight)
        {
            newWidth = desiredWidth;
            newHeight = (originalHeight * desiredWidth + originalWidth / 2) / originalWidth;
        }
        else
        {
            newWidth = desiredHeight;
            newHeight = (originalHeight * desiredWidth + originalWidth / 2) / originalWidth;
        }
    }
    

  33. 老赵
    admin
    链接

    老赵 2009-11-24 18:15:00

    @qiaojie
    兄弟用代码插入功能再发一次吧,呵呵。

  34. qiaojie
    *.*.*.*
    链接

    qiaojie 2009-11-24 18:18:00

    @Jeffrey Zhao
    好吧,改了....

  35. 卡通一下
    *.*.*.*
    链接

    卡通一下 2009-11-24 18:59:00

    没有好好地看文章,只是将自己处理小数的一个方法拿出来。

    SqlDecimal aa = 123.325m;	// 创建一个变量
    
    aa = SqlDecimal.ConvertToPrecScale( aa, 12, 2 );
    
    // 设置变量长度为12位; 小数为2位
    
    

  36. 老赵
    admin
    链接

    老赵 2009-11-24 19:05:00

    @卡通一下
    Math类里不时已经有一些方法了吗?

  37. 卡通一下
    *.*.*.*
    链接

    卡通一下 2009-11-24 19:09:00

    Jeffrey Zhao:
    @卡通一下
    Math类里不时已经有一些方法了吗?


    不好意思,我是后来才知道Math的,可是程序中已经大量地应用自己的方法了。

    但是这个方法还是挺方便的,是从MSDN中找出来的。

  38. 温景良(Jason)
    *.*.*.*
    链接

    温景良(Jason) 2009-11-24 21:19:00

    那是因为你没有做过财务系统:

    温景良(Jason):哎,该死的小数点精度害死我了,系统在设计的时候没考虑那么多,结果算到最后总是差1或者2块,晕死了


    所以你很少关注小数点的问题。


    是啊,我就是没有做过财务系统.

  39. 掐头去尾还是说说你的原话[未注册用户]
    *.*.*.*
    链接

    掐头去尾还是说说你的原话[未注册用户] 2009-11-24 21:40:00

    温景良(Jason):哎,该死的小数点精度害死我了,系统在设计的时候没考虑那么多,结果算到最后总是差1或者2块,晕死了


    做数据类的软件,数据精度是起码的要求之一,也就是说是在架构前就议定好的,应该不会在开发后才发现问题。(当然个别的疏漏还是有的)

    也许是初次做这类软件,但也不会差到1-2元钱。

  40. 小城故事
    *.*.*.*
    链接

    小城故事 2009-11-24 22:09:00

    晕,用Convert.ToInt32就可以了,四舍五入,强制转化是舍尾的。

  41. 是啊,很多基础的东西都被忽略了![未注册用户…
    *.*.*.*
    链接

    是啊,很多基础的东西都被忽略了![未注册用户] 2009-11-24 22:17:00

    小城故事:晕,用Convert.ToInt32就可以了,四舍五入,强制转化是舍尾的。


  42. 烙馅饼喽_2[未注册用户]
    *.*.*.*
    链接

    烙馅饼喽_2[未注册用户] 2009-11-24 22:35:00

    想当年VB6下做用友二次开发,就是用 Currency型的,嘿嘿,用double确实会出问题。不过用Currency就没出过啥问题,其实财务软件因为有财务规则在,所以用了高精度的数据类型其实不会出啥问题,因为毕竟就算人工算,该有小数点还是有小数点,就按照人工计算的规则来就行了。我记得好像U8可以设置小数精度,最多4位吧好像,时间久了记不清了

  43. Nana's Lich
    *.*.*.*
    链接

    Nana's Lich 2009-11-25 05:05:00

    Round并不是四舍五入,MSDN上对它的解释是“此方法的行为遵循 IEEE 标准 754 的第 4 节。这种舍入有时称为就近舍入或四舍六入五成双。它可以将因单方向持续舍入中点值而导致的舍入误差降到最低。”

    Convert.ToInt32的原理也是一样的,而Convert.ToInt32是这样说明的:“如果 value 为两个整数中间的数字,则返回二者中的偶数;即 4.5 转换为 4,而 5.5 转换为 6。 ”

    这个设计和四舍五入有区别,但一般情况下问题不大。以我们在讨论的这个例子来说,如果输入图像的大小是400x249,期望不大于200x200,我们就会得到200x124.5,会被这个算法变成200x124,而如果是四舍五入的话,应该得到200x125。

    感谢楼下纠正,看来Round和我们平时说的“四舍五入”虽然有差别,但这个差别好像可以忽略不计。

  44. 幸存者
    *.*.*.*
    链接

    幸存者 2009-11-25 07:03:00

    @Nana's Lich
    Math.Round(124.9999) = 125
    只有舍入的那一位是5的时候才会考虑前一位的奇偶,也就是说:
    Math.Round(124.5) = 124
    同时还要考虑后面几位,例如
    Math.Round(124.51) = 125

  45. Nana's Lich
    *.*.*.*
    链接

    Nana's Lich 2009-11-25 07:25:00

    幸存者:
    @Nana's Lich
    Math.Round(124.9999) = 125
    只有舍入的那一位是5的时候才会考虑前一位的奇偶,也就是说:
    Math.Round(124.5) = 124
    同时还要考虑后面几位,例如
    Math.Round(124.51) = 125



    的确是这样,谢谢。
    因为没怎么使用过,所以妄下结论了,很抱歉。

  46. 各有各的算法[未注册用户]
    *.*.*.*
    链接

    各有各的算法[未注册用户] 2009-11-25 07:48:00

    烙馅饼喽_2:想当年VB6下做用友二次开发,就是用 Currency型的,嘿嘿,用double确实会出问题。不过用Currency就没出过啥问题,其实财务软件因为有财务规则在,所以用了高精度的数据类型其实不会出啥问题,因为毕竟就算人工算,该有小数点还是有小数点,就按照人工计算的规则来就行了。我记得好像U8可以设置小数精度,最多4位吧好像,时间久了记不清了


    一般地说金额是两位、重量是三位、长度按客户要求...

  47. 温景良(Jason)
    *.*.*.*
    链接

    温景良(Jason) 2009-11-25 09:09:00

    掐头去尾还是说说你的原话:

    温景良(Jason):哎,该死的小数点精度害死我了,系统在设计的时候没考虑那么多,结果算到最后总是差1或者2块,晕死了


    做数据类的软件,数据精度是起码的要求之一,也就是说是在架构前就议定好的,应该不会在开发后才发现问题。(当然个别的疏漏还是有的)

    也许是初次做这类软件,但也不会差到1-2元钱。


    呵呵,因为单位不同,所以精度一开始设置得不一样.一个是万元,设置4位小数,一个单位是万元,设置小数2位,所以精度不一样,之后就对不上了

  48. 卡通一下
    *.*.*.*
    链接

    卡通一下 2009-11-25 09:12:00

    幸存者:
    ......
    只有舍入的那一位是5的时候才会考虑前一位的奇偶,也就是说:
    Math.Round(124.5) = 124
    ......


    用下面这个方法就符合中国人的习惯了(四舍五入):

    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Data.SqlTypes;
    
    namespace ConsoleApplication1
    {
        class Program
        {
            static void Main(string[] args)
            {
                decimal aa = 124.5m;
    
                aa = (Decimal)(SqlDecimal.ConvertToPrecScale(aa,5,0));
    
                Console.WriteLine(aa);
    
                Console.Read();
    
                // 显示为 125
            }
        }
    }
    


    这是另一个方法:

    using System;
    using System.Collections.Generic;
    using System.Text;
    
    namespace ConsoleApplication1
    {
        class Program
        {
            static void Main(string[] args)
            {
                decimal aa = 124.5m;
    
                aa = Convert.ToDecimal(aa.ToString("0."));
    
                Console.WriteLine(aa);
    
                Console.Read();
    
                // 显示为 125
            }
        }
    }
    

  49. 卡通一下
    *.*.*.*
    链接

    卡通一下 2009-11-25 09:32:00

    温景良(Jason):
    ......
    呵呵,因为单位不同,所以精度一开始设置得不一样.一个是万元,设置4位小数,一个单位是万元,设置小数2位,所以精度不一样,之后就对不上了


    只要是金额,它的“基础数据”一定要设置到“分”,个别用户甚至设置到“厘”。

    只是在汇总、显示、打印时,可以用到万元单位(小数位按客户需求)。否则,就会出现误差。

    这也是经验之谈。

  50. 水木
    *.*.*.*
    链接

    水木 2009-11-25 09:48:00

    做过电子商务的园友,一般都会知道这个错误,只是没有仔细分析,呵

  51. Kenet[未注册用户]
    *.*.*.*
    链接

    Kenet[未注册用户] 2009-11-25 10:46:00

    Ivony...:
    因为很多人脑子里对于浮点型和整型的理解就是小数和整数的区别,这是非常错误的!

    浮点型和整型的差别在于近似计算和精确(无损)计算,浮点型只能用于近似计算,其计算结果无精确保证。

    很难以置信还有这么多人将浮点数用于财务运算。当然这与MSDN的误导也不无关系,decimal适合于财务运算。。。。这是非常扯淡的,任何浮点数无论其精度多高,采用十进制还是二进制,在本质上就不能用于精确(无损)计算。



    不用decimal,那用什么?难道用整数做帐吗?麻烦给个方法

  52. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-11-25 11:51:00

    Kenet:

    Ivony...:
    因为很多人脑子里对于浮点型和整型的理解就是小数和整数的区别,这是非常错误的!

    浮点型和整型的差别在于近似计算和精确(无损)计算,浮点型只能用于近似计算,其计算结果无精确保证。

    很难以置信还有这么多人将浮点数用于财务运算。当然这与MSDN的误导也不无关系,decimal适合于财务运算。。。。这是非常扯淡的,任何浮点数无论其精度多高,采用十进制还是二进制,在本质上就不能用于精确(无损)计算。



    不用decimal,那用什么?难道用整数做帐吗?麻烦给个方法




    当然用整数做账,你把钱当成分是基本单位不就行了?有0.5分钱么?
    如果你需要,那就以厘做单位,就这么简单。。。。

  53. Aggron
    *.*.*.*
    链接

    Aggron 2009-11-25 12:00:00

    恩,用整数做账

  54. 幸存者
    *.*.*.*
    链接

    幸存者 2009-11-25 12:27:00

    用整数做帐碰到需要平均值之类的问题时还是不可回避的要与浮点数打交道。

  55. 理论上说说可以,实际应用确不行[未注册用户…
    *.*.*.*
    链接

    理论上说说可以,实际应用确不行[未注册用户] 2009-11-25 12:40:00

    Ivony...:
    ......
    当然用整数做账,你把钱当成分是基本单位不就行了?有0.5分钱么?
    如果你需要,那就以厘做单位,就这么简单。。。。


    对于基础数据的保存可以使用整数类型,只需将数据库中的字段设置成int类型就可以了。

    但是,在实际应用中却行不通。因为,我们大家的习惯都是以元为单位,不习惯以分。我们总不能在屏幕上显示:

    力士香皂 单价 320 分

    所以在实际应用中还是离不开小数点!

  56. 老赵
    admin
    链接

    老赵 2009-11-25 12:43:00

    @理论上说说可以,实际应用确不行
    计算时用整数,显示时加小数点而已。

  57. 难道在字符串中加?[未注册用户]
    *.*.*.*
    链接

    难道在字符串中加?[未注册用户] 2009-11-25 12:45:00

    Jeffrey Zhao:
    @理论上说说可以,实际应用确不行
    计算时用整数,显示时加小数点而已。


  58. 这个问题好像不存在......[未注册用户…
    *.*.*.*
    链接

    这个问题好像不存在......[未注册用户] 2009-11-25 12:53:00

    幸存者:用整数做帐碰到需要平均值之类的问题时还是不可回避的要与浮点数打交道。


    也许是我没有想清楚?

  59. 幸存者
    *.*.*.*
    链接

    幸存者 2009-11-25 12:58:00

    @这个问题好像不存在......
    举个例子,某公司一季度收入1000万整,要算平均每个月的收入,难道用分数表示?

  60. 你没有好好地看帖子![未注册用户]
    *.*.*.*
    链接

    你没有好好地看帖子![未注册用户] 2009-11-25 13:12:00

    幸存者:
    @这个问题好像不存在......
    举个例子,某公司一季度收入1000万整,要算平均每个月的收入,难道用分数表示?


    人家说是用分来表示最小值!(用厘也行)

    1000万 = 1000000000 分

    你自己求一下平均值吧。

  61. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-11-25 13:14:00

    幸存者:
    @这个问题好像不存在......
    举个例子,某公司一季度收入1000万整,要算平均每个月的收入,难道用分数表示?





    整型运算满足如下三个恒等式(不考虑溢出的情况下):

    i + j - j === i;
    i * j / j === i;
    i / j * j + i % j === i

    我们可以看出来在这几个式子中,我们可以得出这样的结论。
    1、如果我们知道i+j和j,则i的值即可确定。
    2、如果我们知道i-j和j,则i的值即可确定。
    3、如果我们知道i*j和j,则i的值即可确定。
    但我们仅知道i/j和j,不可能确定i的值
    而是
    4、如果我们知道i/j和i%j还有j的值,则i可以确定。

    所以在计算平均值这样的场景,我们仅仅保存i/j和j的值,是不够的。如果加上i%j,则i的值也是可逆的。

    所以在除法运算时,为了能够还原计算前的值,我们必须保存余数。

  62. kenet[未注册用户]
    *.*.*.*
    链接

    kenet[未注册用户] 2009-11-25 13:20:00

    @Ivony...
    @Jeffrey Zhao
    5分钱买了3个萝卜,界面显示萝卜均价,
    客户要求价格精确到分,如何显示?

  63. kenet[未注册用户]
    *.*.*.*
    链接

    kenet[未注册用户] 2009-11-25 13:21:00

    而且这个均价是要记到数据库里的,以后可能要用均价进行统计,
    这个用整数计算的想法该在以后系统的统计中如何去实现和被考虑?

  64. 2分一个[未注册用户]
    *.*.*.*
    链接

    2分一个[未注册用户] 2009-11-25 14:57:00

    kenet:
    @Ivony...
    @Jeffrey Zhao
    5分钱买了3个萝卜,界面显示萝卜均价,
    客户要求价格精确到分,如何显示?


  65. Nana's Lich
    *.*.*.*
    链接

    Nana's Lich 2009-12-05 13:06:00

    @卡通一下

    卡通一下:

    幸存者:
    ......
    只有舍入的那一位是5的时候才会考虑前一位的奇偶,也就是说:
    Math.Round(124.5) = 124
    ......


    用下面这个方法就符合中国人的习惯了(四舍五入):

    [code=csharp]
    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Data.SqlTypes;

    namespace ConsoleApplication1
    {
    class Program
    {
    stati...




    IEEE提出的4舍6入五成双是有原因的,的确如文档所说,这样的算法可以把误差降到最小。
    所以如果没有非常之必要,用Round就可以了。

  66. 张荣华
    *.*.*.*
    链接

    张荣华 2009-12-09 10:27:00

    这篇学习了,话说以前还真不知道浮点数除法会有这些问题。

  67. sohigh
    112.97.24.*
    链接

    sohigh 2013-04-03 11:03:01

    感觉没有什么实际意义啊,Python里面的数学模块能很好的做到保持精度。 先除再乘,先乘再除都没有区别.个人愚见!

  68. 老赵
    admin
    链接

    老赵 2013-04-03 16:23:18

    @sohigh

    这点肯定是需要知道的,至于你用数学模块里面保持精度的类型当然没问题了,但是效率也随之刷刷下降,权衡吧。

  69. felix
    110.191.191.*
    链接

    felix 2013-07-21 14:15:05

    还是没搞清楚具体 遇到金额 字段怎么解决,是数据上采用整数存储 还是 小数存储 ,前端 通过 乘 一个 大数 MAX 然后 再除一个 大数 MAX 来解决
    http://blog.csdn.net/sunvince/article/details/6737658

  70. test5
    218.66.59.*
    链接

    test5 2015-10-14 14:44:12

    test5.

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我