银行家舍入VS四舍五入(上):.NET发现之旅(三)-阿里云开发者社区

开发者社区> 开发与运维> 正文
登录阅读全文

银行家舍入VS四舍五入(上):.NET发现之旅(三)

简介:


昨天和IBM以前培训的一个学员聊天,他问我.NET Framework提供四舍五入的方法了吗?我当时解释了老半天,索性就直接写一篇博文吧,以后学生问起,直接发文章看。

 
提到四舍五入,处在我们这个年龄层的人应该都很清楚,因为我们当时的小学教育灌输的就是四舍五入。但是如果提到银行家舍入,也许很多朋友会一下子愣住。银行家舍入,英文名为Banker's round,它实现的舍入效果是“四舍六入五取偶”。银行家舍入是IEEE规定的小数舍入标准之一,也是IEEE目前规定中最优秀的舍入方法,因此所有符合 IEEE 标准的语言都应该实现这种算法,.NET平台也不例外。
 
这两节涉及的内容如下:
一,银行家舍入和四舍五入比较
二,.NET平台中的银行家舍入和四舍五入
三,自己动手开发四舍五入组件
 
 
一,银行家舍入和四舍五入比较
 
首先我们比较它们的规则:
1,  四舍五入
当舍去位的数值大于等于5时,在舍去该位的同时向前位进一;当舍去位的数值小于5时,则直接舍去该位。
2,  银行家舍入
所谓银行家舍入法,其实质是一种四舍六入五取偶(又称四舍六入五留双)法。其规则是:当舍去位的数值小于5时,直接舍去该位;当舍去位的数值大于等于6时,在舍去该位的同时向前位进一;当舍去位的数值等于5时,如果前位数值为奇,则在舍去该位的同时向前位进一,如果前位数值为偶,则直接舍去该位。
 
例如:我们对2.3352.3452.3642.3662.367分别进行四舍五入和银行家舍入
 
四舍五入后的结果:
2.335à2.34
2.345à2.35
2.364à2.36
2.366à2.37
2.367à2.37
 
银行家舍入后的结果:
2.335à2.34
2.345à2.34
2.364à2.36
2.366à2.37
2.367à2.37
 
怎么比较它们孰优孰劣呢?
对于123456789一系列数,它们出现的随机可能性几乎一样的。所以如果用四舍五入进行舍取,那么5左右两面奇偶的平衡性就不好,59都进位,而14都舍去。如果利用银行家算法,14都舍去,69都进位,各自四位,然后把5分成两种情况,前位如果是奇数就取偶,如果是偶数就保留,我们发现5前面和5后面奇偶数的个数刚好一样,所以与四舍五入比较,此时的银行家舍入的公平性更强。
 
 
二,.NET平台中的银行家舍入和四舍五入
 
在探明.NET平台的银行家舍入和四舍五入之前,我们先来探讨在实际开发中,我们习惯用什么类型的小数。我想大家可能比较习惯选doubledecimal类型,当然float类型也是一样。其中doublefloat是浮点型,而decimal类型是货币类型,它比浮点型更精确,同时decimal类型既不是浮点型也不是整形。我们先看.NET中处理decimal类型的一个例子:
 
InBlock.gifusing System; 
InBlock.gifusing System.Collections.Generic; 
InBlock.gifusing System.Text; 
InBlock.gif 
InBlock.gifnamespace ConsoleApplication2 
InBlock.gif
InBlock.gif        class Program 
InBlock.gif        { 
InBlock.gif                static void Main(string[] args) 
InBlock.gif                { 
InBlock.gif                        //Floor方法向负无穷方向舍入为最接近的整数 
InBlock.gif                        Console.WriteLine(decimal.Floor(-1.3m)); //-2 
InBlock.gif                        Console.WriteLine(decimal.Floor(3.5m)); //3 
InBlock.gif                        Console.WriteLine(decimal.Floor(4m));     //4 
InBlock.gif 
InBlock.gif                        //Truncate方法向零方向舍入为整数 
InBlock.gif                        Console.WriteLine(decimal.Truncate(-1.3m));//-1 
InBlock.gif                        Console.WriteLine(decimal.Truncate(3.5m));//3 
InBlock.gif                        Console.WriteLine(decimal.Truncate(4m));    //4 
InBlock.gif 
InBlock.gif                        //实现正的decimal数值的四舍五入,则必须用下面的技巧,保留到小数点后2位,就用100,保留到小数点后3位就用1000,依次类推 
InBlock.gif                        decimal a = 8.335m, b = 8.345m; 
InBlock.gif                        Console.WriteLine(decimal.Truncate(a * 100 + 0.5m) / 100); //8.34 
InBlock.gif                        Console.WriteLine(decimal.Truncate(b * 100 + 0.5m) / 100); //8.35 
InBlock.gif 
InBlock.gif                        //实现负的decimal数值的四舍五入,则必须用下面的技巧,保留到小数点后2位,就用100,保留到小数点后3位就用1000,依次类推 
InBlock.gif                        decimal c = -8.335m, d = -8.345m; 
InBlock.gif                        Console.WriteLine(decimal.Truncate(c * 100 - 0.5m) / 100); //8.34 
InBlock.gif                        Console.WriteLine(decimal.Truncate(d * 100 - 0.5m) / 100); //8.35 
InBlock.gif                        Console.WriteLine(); 
InBlock.gif                } 
InBlock.gif        } 
InBlock.gif
 
从上面这个例子看出decimal类型的Floor方法Truncate方法比较特殊,而如果想实现decimal类型的四舍五入,我们这里用的是Truncate方法和数学计算的形式实现的四舍五入。如果查看MSDN,会发现System.Decimal结构比System.Double结构以及System.Single结构提供的方法要多一些。事实上System.Double结构和System.Single结构中本身没提供Floor方法Truncate方法。这也是decimal数据类型比较有优势的地方,但是,System.Math结构却为我们提供了double类型对应的Floor方法Truncate方法。所以可以利用Math结构的Floor方法Truncate方法实现对double类型的小数处理以及四舍五入,功能和System.Decimal结构提供的Floor方法Truncate方法一样,而float类型可以隐式转换为double类型,来实现float类型的小数处理以及四舍五入。另外,Math结构的Floor方法Truncate方法还有对应的处理decimal类型的重载形式,功能和System.Decimal结构提供的Floor方法Truncate方法一样。
细心的开发人员一定会发现System.Decimal结构以及System.Math还提供了一个Round方法,一旦你使用了Round方法,你便会产生很多疑问,这也是IBM那位学员产生疑惑的地方。没错,.NET平台的Round方法确实让人着实的迷惑,但事实上,如果你一旦知道.NET平台的Round方法是对IEEE规定的银行家舍入标准的实现,迷惑便会减少。System.DecimalRound方法有四种重载形式,其中有三种重载形式最经常用。同样,System.Math结构也提供了Round方法,它和System.DecimalRound方法区别是,System.Math结构的Round方法不仅提供了处理decimal类型的版本,而且还提供了处理double类型的版本,所以System.Math结构的Round方法有八种重载形式。
如果你看完了下面这个例子,那么.NET平台中怎么实现银行家舍入和四舍五入也便搞懂了。这个例子我们分别用了System.DecimalRound方法其中三种重载形式,以及System.Math结构的Round方法decimal类型和double类型的重载形式。有个小知识必选明白,C#中定义一个decimal类型的值末尾必须加mM,同时小数默认的类型是double类型,所以凡是你看到的末尾没加mM的小数都是double类型。
InBlock.gifusing System; 
InBlock.gifusing System.Collections.Generic; 
InBlock.gifusing System.Text; 
InBlock.gif 
InBlock.gifnamespace ConsoleApplication2 
InBlock.gif
InBlock.gif        class Program 
InBlock.gif        { 
InBlock.gif                static void Main(string[] args) 
InBlock.gif                { 
InBlock.gif                        Console.WriteLine(decimal.Round(3.5m));//四舍六入五取偶 
InBlock.gif                        Console.WriteLine(decimal.Round(4.5m));//四舍六入五取偶 
InBlock.gif                        Console.WriteLine(decimal.Round(4.6m));//四舍六入五取偶 
InBlock.gif                        Console.WriteLine(decimal.Round(3.6m));//四舍六入五取偶 
InBlock.gif                        Console.WriteLine(decimal.Round(4.7m));//四舍六入五取偶 
InBlock.gif                        Console.WriteLine(); 
InBlock.gif 
InBlock.gif                        Console.WriteLine(Math.Round(3.5m));//四舍六入五取偶 
InBlock.gif                        Console.WriteLine(Math.Round(4.5m));//四舍六入五取偶 
InBlock.gif                        Console.WriteLine(Math.Round(4.6m));//四舍六入五取偶 
InBlock.gif                        Console.WriteLine(Math.Round(3.6m));//四舍六入五取偶 
InBlock.gif                        Console.WriteLine(Math.Round(4.7m));//四舍六入五取偶 
InBlock.gif                        Console.WriteLine(); 
InBlock.gif 
InBlock.gif                        Console.WriteLine(Math.Round(3.5));//四舍六入五取偶 
InBlock.gif                        Console.WriteLine(Math.Round(4.5));//四舍六入五取偶 
InBlock.gif                        Console.WriteLine(Math.Round(4.6));//四舍六入五取偶 
InBlock.gif                        Console.WriteLine(Math.Round(3.6));//四舍六入五取偶 
InBlock.gif                        Console.WriteLine(Math.Round(4.7));//四舍六入五取偶 
InBlock.gif                        Console.WriteLine(); 
InBlock.gif 
InBlock.gif 
InBlock.gif 
InBlock.gif                        Console.WriteLine(decimal.Round(0.125m,2));//四舍六入五取偶 
InBlock.gif                        Console.WriteLine(decimal.Round(0.135m, 2));//四舍六入五取偶 
InBlock.gif                        Console.WriteLine(decimal.Round(0.126m, 2));//四舍六入五取偶 
InBlock.gif                        Console.WriteLine(decimal.Round(-3.105m, 2, MidpointRounding.AwayFromZero));//四舍五入 
InBlock.gif                        Console.WriteLine(decimal.Round(-0.5m, 0, MidpointRounding.AwayFromZero));//四舍五入 
InBlock.gif                        Console.WriteLine(); 
InBlock.gif 
InBlock.gif                        Console.WriteLine(Math.Round(0.125m, 2));//四舍六入五取偶 
InBlock.gif                        Console.WriteLine(Math.Round(0.135m, 2));//四舍六入五取偶 
InBlock.gif                        Console.WriteLine(Math.Round(0.126m, 2));//四舍六入五取偶 
InBlock.gif                        Console.WriteLine(Math.Round(-3.105m, 2, MidpointRounding.AwayFromZero));//四舍五入 
InBlock.gif                        Console.WriteLine(Math.Round(-0.5m, 0, MidpointRounding.AwayFromZero));//四舍五入 
InBlock.gif                        Console.WriteLine(); 
InBlock.gif 
InBlock.gif                        Console.WriteLine(Math.Round(0.125, 2));//四舍六入五取偶 
InBlock.gif                        Console.WriteLine(Math.Round(0.135, 2));//四舍六入五取偶 
InBlock.gif                        Console.WriteLine(Math.Round(0.126, 2));//四舍六入五取偶 
InBlock.gif                        Console.WriteLine(Math.Round(-3.105, 2, MidpointRounding.AwayFromZero));//四舍五入 
InBlock.gif                        Console.WriteLine(Math.Round(-0.5, 0, MidpointRounding.AwayFromZero));//四舍五入 
InBlock.gif                        Console.WriteLine(); 
InBlock.gif                } 
InBlock.gif        } 
InBlock.gif
 
最终打印的结果如下图
 
从上面这个例子,我们可以看到System.DecimalRound方法System.Math结构的Round方法默认情况下,都是采用的银行家舍入,即四舍六入五取偶,其中带一个参数的重载和带连个参数的重载形式下,默认都是银行家舍入。比较特殊的是带三个参数的Round方法,它的第三个参数是MidpointRounding枚举类型,MidpointRounding枚举类型定义了两个枚举值,为了实现四舍五入,我们这里使用的是MidpointRounding.AwayFromZero这个值。中文版的MSND对这个枚举值的解释是:当一个数字是其他两个数字的中间值时,会将其舍入为两个值中绝对值较的值。事实上这是错误的!经过测试,MidpointRounding.AwayFromZero枚举值的真正意思是:当一个数字是其他两个数字的中间值时,会将其舍入为两个值中绝对值较的值。所以MSDN这里是翻译错误,J
所以我们既可以使用Round方法的前两种重载形式实现银行家舍入,也可以使用带三个参数的Round方法实现四舍五入,但枚举值必须使用MidpointRounding.AwayFromZero,而不能使用MidpointRounding.ToEven
OK,讲到此为止,我们知道.NET平台不仅提供了银行家舍入的方法,而且也提供了四舍五入的方法。
















本文转自terryli51CTO博客,原文链接:http://blog.51cto.com/terryli/154840 ,如需转载请自行联系原作者


版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:
开发与运维
使用钉钉扫一扫加入圈子
+ 订阅

集结各类场景实战经验,助你开发运维畅行无忧

其他文章