品人 發表於 2024-6-3 08:00:00

C#.Net筑基-深入解密小数内部存储的秘密

<p><img src="https://img2024.cnblogs.com/blog/151257/202405/151257-20240529214214932-2068473893.png" alt="image.png" loading="lazy"></p>
<p>为什么<code>0.1 + 0.2</code> 不等于 <code>0.3</code>?为什么<code>16777216f</code> 等于 <code>16777217f</code>?为什么金钱计算都推荐用<code>decimal</code>?本文主要学习了解一下数字背后不为人知的存储秘密。</p>
<hr>
<h1 id="01数值类型">01、数值类型</h1>
<p>C#中的数字类型主要包含两类,整数、小数,C#中的小数都为浮点(小)数。</p>
<p><img src="https://img2024.cnblogs.com/blog/151257/202405/151257-20240529214214911-589227263.png" alt="" loading="lazy"></p>
<pre><code class="language-csharp">void Main()
{
        int a1 = 100;
        int a2 = 0x0f; //15
        var b2 = 0b11; //3
        var x1 = 1;    //整数值默认为int
        var y1 = 1.1;//小数值默认为double
        Add(1, 2.3); //3.3
        Add(1, 3);   //4
}
private T Add&lt;T&gt;(T x, T y) where T : INumber&lt;T&gt;
{
        return x + y * x;
}
</code></pre>
<ul>
<li>用<code>var</code>类型推断时,整数值默认为<code>int</code>,小数值默认为<code>double</code>。</li>
<li><code>.NET 7</code> 新增的一个专门用来约束数字类型的接口 INumber<code>&lt;T&gt;</code> ,用来约束数字类型非常好用。</li>
</ul>
<p>数值类型大多提供的成员:</p>
<table>
<thead>
<tr>
<th><strong>🔸静态字段</strong></th>
<th><strong>说明</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>MaxValue</td>
<td>最大值常量,<code>Console.WriteLine(int.MaxValue);   //2147483647</code></td>
</tr>
<tr>
<td>MinValue</td>
<td>最小值常量</td>
</tr>
<tr>
<td><strong>🔸静态方法</strong></td>
<td><strong>说明</strong></td>
</tr>
<tr>
<td><strong>Parse、TryParse</strong></td>
<td>转换为数值类型,是比较常用的类型转换函数,参数<code>NumberStyles</code>可定义解析的数字格式</td>
</tr>
<tr>
<td>Max、Min</td>
<td>比较值的大小,返回最大、小的值,<code>int.Max(1,100) //100</code></td>
</tr>
<tr>
<td>Abs</td>
<td>计算绝对值</td>
</tr>
<tr>
<td>IsInfinity</td>
<td>是否有效值,无穷值</td>
</tr>
<tr>
<td>IsInteger</td>
<td>是否整数</td>
</tr>
<tr>
<td>IsNaN</td>
<td>是否为NaN</td>
</tr>
<tr>
<td>IsPositive</td>
<td>是否零或正实数</td>
</tr>
<tr>
<td>IsNegative</td>
<td>是否表示负实数</td>
</tr>
</tbody>
</table>
<p>数值类型还有很多接口,如加、减、乘、除的操作符接口,作为泛型约束条件使用还是挺不错的。</p>
<table>
<thead>
<tr>
<th><strong>🔸操作符接口</strong></th>
<th><strong>说明</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>IAdditionOperators</td>
<td>加法</td>
</tr>
<tr>
<td>ISubtractionOperators</td>
<td>减法</td>
</tr>
<tr>
<td>IMultiplyOperators</td>
<td>乘法</td>
</tr>
<tr>
<td>IDivisionOperators</td>
<td>除法</td>
</tr>
</tbody>
</table>
<pre><code class="language-csharp">public static T Power&lt;T&gt;(T v1, T v2) where T : INumber&lt;T&gt;,
        IMultiplyOperators&lt;T, T, T&gt;, IAdditionOperators&lt;T, T, T&gt;
{
        return v1 * v1 + v2 * v2;
}
</code></pre>
<hr>
<h1 id="02小数浮点数">02、小数、浮点数⁉</h1>
<p>C#中的小数类型有float、double、decimal 都是浮点数,浮点 就是“ <strong>浮动小数点位置</strong>”,小数位数不固定,小数部分、整数部分是共享数据存储空间的。相应的,自然也有<strong>定点小数</strong>,固定小数位数,在很多数据库中有定点小数,C#中并没有。</p>
<p>在编码中我们常用的浮点小数是float、double,经常会遇到精度问题,以及类似下面这些面试题。</p>
<ul>
<li>❓ 为什么<code>0.1 + 0.2</code> 不等于 <code>0.3</code>?</li>
<li>❓ 为什么浮点数无法准确的表示 <code>0.1</code>?</li>
<li>❓ 为什么<code>16777216f</code> 等于 <code>16777217f</code>?这里<code>f</code>表示为<code>float</code>。</li>
<li>❓ 为什么<code>32</code>位<code>float</code>可以最大表示<code>3.402823E38</code>,<code>64</code>位<code>double</code>可以最大表示<code>1.79*E308</code>,那么点位数根本存不下啊?</li>
<li>❓ 同样是32位,<code>float</code>的数据范围远超<code>int</code>,为什么?</li>
</ul>
<p><img src="https://img2024.cnblogs.com/blog/151257/202405/151257-20240529214215377-72131815.png" alt="image.png" loading="lazy"></p>
<pre><code class="language-csharp">Console.WriteLine(0.1 + 0.2 == 0.3);       //False
Console.WriteLine(16777216f == 16777217f); //True
Console.WriteLine(double.MaxValue); //1.7976931348623157E+308
Console.WriteLine(int.MaxValue);    //2147483647
Console.WriteLine(sizeof(double));//8 //8字节(64位)
</code></pre>
<p>float、double为浮点数,小数位数有限,比较容易损失精度。造成上面这些问题的根本原因是其存储机制决定的,他们都遵循IEEE754格式规范,几乎所有编程语言和处理器都支持该规范,因此大多数编程语言都有类似的问题。Decimal 为高精度浮点数,存储机制与float、double不同,她采用十进制方式表示。</p>
<blockquote>
<p>❗ 要搞懂float、double,就不得不了解IEEE754规范!</p>
</blockquote>
<h2 id="21ieee754floatdouble存储原理">2.1、IEEE754:float、double存储原理</h2>
<p><strong>IEEE 754</strong> (维基百科)是一个关于浮点数算术的国际标准,它定义了浮点数的表示格式、舍入规则、特殊值、浮点运算等规范。IEEE 754 标准最早发布与1985年,其中包括了四种精度规范,其中最常用的就两种:<strong>单精度</strong>(float,4字节32位)和<strong>双精度</strong>(double,8字节64位)。大多数编程语言、硬件处理器都支持这两种浮点数据类型,因此float、double的知识几乎是所有语言通用的,可以深入了解一下,不亏的!</p>
<p>IEEE 754 浮点数不像十进制字面量值那样存储,而是用下面的二进制方式来表示并存储的,其实就是二进制的科学计数法。其二进制表示包含三个部分:<strong>符号位S</strong>、<strong>指数部分</strong>(阶码E,2为底的指数)和<strong>尾数部分M</strong>。</p>
<p><img src="https://img2024.cnblogs.com/blog/151257/202405/151257-20240529214215316-113629811.png" alt="" loading="lazy"></p>
<ul>
<li>
<p>🔸<strong>符号位(Sign)</strong>:占用1位,这是浮点数的最高位,用于表示数字的正负。0表示正数,1表示负数。</p>
</li>
<li>
<p>🔸<strong>指数部分(Exponent,阶码)</strong>:表示为2位底的指数,这里使用了移码,实际的指数<code>e = E-127</code>,这样省去了指数的符号位,计算也更方便。</p>
<ul>
<li><strong>float</strong> 的指数部分8位,<code>2^8=256</code> 偏移量(移码)为127,表示十进制范围为 [-127,128],其数据范围就为 <code>±2^128</code> = <code>±3.4E38</code>。指数全是1即指数值为255时,表示为无效数字 ±infinity或NaN。</li>
<li><strong>double</strong> 的指数部分11位,<code>2^11=2048</code> 偏移量(移码)为1023,十进制值范围[-1023,1024],因此数据范围 <code>±2^1024 = ±1.79E308</code>。</li>
</ul>
</li>
<li>
<p>🔸<strong>尾数部分(Mantissa)</strong>:这部分表示数字的精确值(有效数字),包括整数和小数部分。尾数长度决定了精度,因为有效数字长度是有限的,因此就必然存在精度丢失的问题。</p>
<ul>
<li><strong>float</strong> 的尾数部分23位,十进制 <code>2^23=8388608</code>,最多6~7(不完整的第7位)位有效十进制数字,只有前6位是完整的。</li>
<li><strong>double</strong> 尾数长度52位,<code>2^52 = 4503599627370496</code>,因此最多有15~16 位有效十进制数字。</li>
</ul>
</li>
</ul>
<p><img src="https://img2024.cnblogs.com/blog/151257/202405/151257-20240529214215309-893225997.png" alt="image.png" loading="lazy"></p>
<p>IEEE754浮点数都会被转换为上述二进制形式:<code>**符号*尾数*2^指数**</code>,如 <code>2 = 1.0 * 2^1</code>,<code>0.5 = 1.0 * 2^-1</code>,<code>5 = 1.25* 2^2</code>。数据(整数、小数部分)先转换为二进制形式,然后左移或右移小数点,转换为<code>1.M</code>形式,始终都是 “1”开头,因此就只存储小数部分即可。</p>
<blockquote>
<p>🚩浮点数 = <img src="https://img2024.cnblogs.com/blog/151257/202405/151257-20240529214214911-1368806345.svg" alt="" loading="lazy"></p>
</blockquote>
<p><img src="https://img2024.cnblogs.com/blog/151257/202405/151257-20240529214215169-2092151273.png" alt="image.png" loading="lazy"><br>
十进制 2 就表示为 <code>2 = 1.0* 2^1</code>。下图来自 在线IEEE754转换器计算:IEEE-754 Floating Point Converter。</p>
<ul>
<li>阶码 <code>E = 127+1 = 128</code>(实际指数e=1) 。</li>
<li>尾数 <code>1.0</code>,实际存储的尾数就是<code>0</code>。</li>
</ul>
<p><img src="https://img2024.cnblogs.com/blog/151257/202405/151257-20240529214215320-432317616.png" alt="image.png" loading="lazy"></p>
<p><img src="https://img2024.cnblogs.com/blog/151257/202405/151257-20240529214215209-1219462028.png" alt="image.png" loading="lazy"></p>
<p>十进制 0.75 表示为<code>0.75 = 1.5* 2^-1</code>,指数为<code>-1</code>,尾数为<code>1.5</code>。</p>
<ul>
<li>阶码 <code>E = 127+ (-1) = 126</code>(实际指数e=-1) 。</li>
<li>尾数 <code>1.5</code>,实际存储的尾数就是<code>0.5</code>,二进制值为<code>0.1</code>。为什么0.5 的二进制为0.1呢,请看后续章节。</li>
</ul>
<p><img src="https://img2024.cnblogs.com/blog/151257/202405/151257-20240529214215437-246411533.png" alt="image.png" loading="lazy"></p>
<h2 id="22floatdouble对比">2.2、float、double对比</h2>
<table>
<thead>
<tr>
<th><strong>类型</strong></th>
<th><strong>单精度 float</strong></th>
<th><strong>双精度 double</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>CTS类型</td>
<td>System.Single</td>
<td>System.Double</td>
</tr>
<tr>
<td>长度</td>
<td>4字节32位</td>
<td>8字节64位</td>
</tr>
<tr>
<td>符号位S</td>
<td>1</td>
<td>1</td>
</tr>
<tr>
<td>阶码(指数位T)</td>
<td>8,[-127,128]</td>
<td>11,[-1023,1024]</td>
</tr>
<tr>
<td>尾数M</td>
<td>23</td>
<td>52</td>
</tr>
<tr>
<td>阶码偏移量</td>
<td>127,<code>e= E -127</code></td>
<td>1023,<code>e= E -1023</code></td>
</tr>
<tr>
<td>精度(10进制)</td>
<td>**6~7 **,<code>2^23=8388608</code></td>
<td><strong>15~16</strong>,<code>2^52 = 4503599627370496</code></td>
</tr>
<tr>
<td>范围</td>
<td>±3.402823E38 ,<code>2^128=3.4E38</code></td>
<td>±1.79*E308,<code>2^1024=1.79E308</code></td>
</tr>
<tr>
<td>字面量表示(后缀)</td>
<td><code>f</code>/<code>F</code></td>
<td><code>d</code>/<code>D</code></td>
</tr>
</tbody>
</table>
<p><img src="https://img2024.cnblogs.com/blog/151257/202405/151257-20240529214215210-998318658.png" alt="image.png" loading="lazy"></p>
<p>float只能用于 表示6~7个有效数字时,才不会损失精度。</p>
<pre><code class="language-csharp">//7位有效数字
Console.WriteLine(4234567f);//4234567
//第8位就不准确了
Console.WriteLine(42345678f); //42345680
Console.WriteLine(42345671f); //42345670

//7位有效数字
Console.WriteLine(0.2345678f);//0.2345678
//第8位就不准确了
Console.WriteLine(2.12345678f); //2.1234567
Console.WriteLine(0.212345678f); //0.21234567
</code></pre>
<h2 id="23小数是怎么转换为二进制的">2.3、小数是怎么转换为二进制的?</h2>
<p>对于整数转换小数是非常容易理解的,计算机的二进制是天然支持整数存储为二进制的。十进制整数转成二进制通常采用 ”除 2 取余,逆序排列” 即可。</p>
<pre><code class="language-csharp">Console.WriteLine($"{1:B4}"); //0001
Console.WriteLine($"{2:B4}"); //0010
Console.WriteLine($"{3:B4}"); //0011
Console.WriteLine($"{4:B4}"); //0100
Console.WriteLine($"{5:B4}"); //0101
Console.WriteLine($"{8:B4}"); //1000
</code></pre>
<blockquote>
<p>📢“B”格式只支持整数,更多格式化参考《String字符串全面了解&gt;字符串格式化大全》</p>
</blockquote>
<h3 id="乘2取整法">🚩乘2取整法</h3>
<p>但小数则不同,采用的是 “<strong>乘2取整法</strong>”,小数部分循环迭代,直到小数部分<code>=0</code>为止。:如下<code>0.875</code>的十进制浮点数转换为二进制格式为:<code>0.111</code>。</p>
<p><img src="https://img2024.cnblogs.com/blog/151257/202405/151257-20240529214215436-477498553.png" alt="image.png" loading="lazy"></p>
<p><code>0.111</code>,存储为IEE754浮点数,转换为<code>1.M*2^E</code>结构,小数点右移一位,就是<code>1.11*2^-1</code>。</p>
<ul>
<li>指数E = <code>-1 + 127</code> = 126 ,二进制值为<code>01111110</code>。</li>
<li>尾数为 <code>11</code> 后面补0。</li>
</ul>
<p><img src="https://img2024.cnblogs.com/blog/151257/202405/151257-20240529214215191-1405984636.png" alt="image.png" loading="lazy"></p>
<p><img src="https://img2024.cnblogs.com/blog/151257/202405/151257-20240529214215365-2074929768.png" alt="image.png" loading="lazy"></p>
<p>十进制小数<code>6.36</code> 转换为二进制,整数部分+小数部分分别转换后合体:</p>
<p><img src="https://img2024.cnblogs.com/blog/151257/202405/151257-20240529214215247-1877708034.png" alt="image.png" loading="lazy"></p>
<h3 id="无限循环的01">🚩无限循环的0.1!</h3>
<p>二进制无法准确表示小数<code>0.1</code>,是因为<code>0.1</code> 转换为二进制后是无限循环的,<code>0.0 0011 0011 0011...</code>,“0011”无限循环。就像十进制小数<code>1/3 = 0.333</code> 一样。</p>
<p><img src="https://img2024.cnblogs.com/blog/151257/202405/151257-20240529214215488-2088277842.jpg" alt="" loading="lazy"></p>
<p>转换为<code>1.M*2^E</code>结构,小数点右移4位,尾数就是<code>1.1001 1001</code>,指数 E = <code>-4 +127 = 123</code>。</p>
<p><img src="https://img2024.cnblogs.com/blog/151257/202405/151257-20240529214215233-486885798.png" alt="image.png" loading="lazy"></p>
<h2 id="24浮点数的精度是怎么回事">2.4、浮点数的精度是怎么回事?</h2>
<p>计算机存储整数很简单,每个数字是确定的。但小数则不同,0到1之间的小数都无限种可能,<strong>计算机有限的空间无法存储无限的小数</strong>。因此计算机将小数也当成“离散”的值,就像整数那样,整数之间间隔始终为1。给小数一个间隔刻度,如下图,用钟表来举例,小数刻度(步进)为0.234(十进制)。</p>
<p><img src="https://img2024.cnblogs.com/blog/151257/202405/151257-20240529214215423-3233259.png" alt="image.png" loading="lazy"></p>
<p>这样做的好处可以兼顾“所有”小数,小数的精度就取决于钟表的“刻度”,刻度越小,精度越高,当然存储时所需要的空间也就越大。</p>
<p><img src="https://img2024.cnblogs.com/blog/151257/202405/151257-20240529214215241-1972839245.png" alt="image.png" loading="lazy"></p>
<p>因此,这个精度本质上是由表盘间隔刻度(Gap)决定的,即使<code>0.0012</code>的间隔刻度,精度达到了4位十进制数,也只能保障前2~3位小数是可靠的。0.001X、0.002X、0.003X,他始终无法表示0.0013、0.0025。</p>
<p>可通过提高刻度(Gap)来提高精度,但存储长度是有限的,因此不管是那种浮点数都是有精度限制的。精度越高的数据类型,也需要更多的长度来存储数据。</p>
<p><img src="https://img2024.cnblogs.com/blog/151257/202405/151257-20240529214215433-1045672239.png" alt="image.png" loading="lazy"></p>
<p>32位<code>float</code> 用了23位来存储有效数字,十进制也就6~7位(<code>2^23=8388608 </code>)。在IEEE754规范中,小数的“刻度”并不是均匀分布的,而是越来越大,数值越大则精度越低。如下面的表盘和刻度尺的示意图,其精度(Gap)的分布是不均匀的,<code>0</code>附近数字的精度最高,然后精度就越来越低了,低到超过1。</p>
<p><img src="https://img2024.cnblogs.com/blog/151257/202405/151257-20240529214215327-211432758.png" alt="image.png" loading="lazy"></p>
<p><img src="https://img2024.cnblogs.com/blog/151257/202405/151257-20240529214218916-1795291652.png" alt="" loading="lazy"></p>
<p>看看 float 的间隔刻度(Gap)如下图,来自官方IEEE_754文档:</p>
<p><img src="https://img2024.cnblogs.com/blog/151257/202405/151257-20240529214215242-71567771.png" alt="image.png" loading="lazy"></p>
<ul>
<li>当数值大于8388608时,刻度(Gap)为1,就不能包含小数了。</li>
<li>当数字大于16777216(1600+万)时间隔刻度为2,连整数精度都不能保证了😂。</li>
</ul>
<pre><code class="language-csharp">//float大于8388608后的间隔为1
Console.WriteLine(8388608.1f == 8388608.4f); //True
//大于16777216后的间隔为2
Console.WriteLine(16777216f == 16777217f); //True
Console.WriteLine(16777218f == 16777219f); //False
Console.WriteLine(16777219f == 16777220f); //True
</code></pre>
<p>下图是double的刻度表:小于8的数字都能有16位精度。</p>
<p><img src="https://img2024.cnblogs.com/blog/151257/202405/151257-20240529214215400-1474423862.png" alt="image.png" loading="lazy"></p>
<blockquote>
<p>😂 怎么感觉float很鸡肋呢?限制太多了!所以编程中浮点数多大都用的 double 居多,float比较少。</p>
</blockquote>
<hr>
<h1 id="03更精确的-decimal">03、更精确的 Decimal</h1>
<p>System.Decimal 是16字节(128位)的<strong>高精度十进制浮点数</strong>,不同于float、double 的二进制存储机制,Decimal 采用10进制存储,表示-7.9E28 到 +7.9E28之间的十进制数。Decimal 最大限度地减少了因舍入而导致的错误,比较适用于对精度要求高场景,如财务计算。</p>
<blockquote>
<p>📢 Decimal并不属于IEEE754规范,也不是处理器支持的类型,计算性能要差一点点(约 double 的 10%)。</p>
</blockquote>
<pre><code class="language-csharp">Console.WriteLine(1f / 3f * 3f); //1
Console.WriteLine(0.1 + 0.2 == 0.3); //False
//decimal更高精度
Console.WriteLine(1m / 3m * 3m); //0.9999999999999999999999999999
Console.WriteLine(0.1m + 0.2m == 0.3m); //True
</code></pre>
<p>Decimal可以准确的表示<code>0.1</code>,Decimal 128位的存储结构如下图(图来源):</p>
<ul>
<li><strong>96位</strong>存储一个大整数,就是有效数字,<code>Math.Pow(2,96) = 7.9E28</code>,最多28位有效数字,因此小数最多也就是28位(全是小数时)。</li>
<li>剩下的<strong>32位</strong>中,有一个符号位,0 表示正数,1 表示负数。其中有<code>5</code>位(下图中的第111位)表示10的指数部分(0到28的整数),可以理解为小数点的位置,其他位数没有使用默认为0(有点浪费呢?)。</li>
</ul>
<p><img src="https://img2024.cnblogs.com/blog/151257/202405/151257-20240529214215241-901794339.png" alt="image.png" loading="lazy"></p>
<p>Decimal 表示小数其实是“障眼法”,内部有三个int (High、Mid、Low)来表示96位有效数字,还有一个int表示指数。可以通过 <code>decimal.GetBits()</code>方法获取他们的值。下图来自 Decimal 源码 Decimal.cs</p>
<p><img src="https://img2024.cnblogs.com/blog/151257/202405/151257-20240529214215428-278441603.png" alt="image.png" loading="lazy"><img src="https://img2024.cnblogs.com/blog/151257/202405/151257-20240529214215213-666282921.png" alt="image.png" loading="lazy"></p>
<h2 id="31为什么decimal没有01问题">3.1、为什么Decimal没有0.1问题?</h2>
<p>在Decimal中就没有 <code>0.1+0.2</code> 不等于<code>0.3</code> 的问题,因为她能准确表示<code>0.1</code>。</p>
<p>其根本原因就是 Decimal 不会把小数转换为二进制,而是就用十进制。把小数都转为整数存储,如 <code>0.1</code>在Decimal 中会被表示为 <code>1* 10^-1</code>,尾数为1,指数为<code>-1</code>,<strong>指数就是小数点位置</strong>。</p>
<blockquote>
<p>📢 Decimal值 = <img src="https://img2024.cnblogs.com/blog/151257/202405/151257-20240529214215366-1959542365.svg" alt="" loading="lazy"></p>
</blockquote>
<pre><code class="language-csharp">var arr = decimal.GetBits(0.1M);       
Console.WriteLine($"尾数:{arr}{arr}{arr}");
Console.WriteLine($"指数:"+$"{arr:B32}".Substring(0,16));
//尾数:001
//指数:0000000000000001
</code></pre>
<p><code>100.1024</code> 存储为<code>1001024* 10^-4</code>。</p>
<ul>
<li>尾数为<code>1001024</code>,全都转换为整数了。不用担心超出整数int范围,<code>96</code>位有三个整数并行存储呢!</li>
<li>指数为<code>4</code>,小数点位置在第四格。</li>
</ul>
<pre><code class="language-csharp">var arr = decimal.GetBits(100.1024M);       
Console.WriteLine($"尾数:{arr}{arr}{arr}");
Console.WriteLine($"指数:"+$"{arr:B32}".Substring(0,16));
//尾数:001001024
//指数:0000000000000100
</code></pre>
<p>如果是负数<code>-100.1024</code>,则只有符号位为<code>1</code>,其他一样</p>
<pre><code class="language-csharp">var arr = decimal.GetBits(-100.1024M);       
Console.WriteLine($"尾数:{arr}{arr}{arr}");
Console.WriteLine($"指数:"+$"{arr:B32}".Substring(0,16));
//尾数:001001024
//指数:1000000000000100
</code></pre>
<p><img src="https://img2024.cnblogs.com/blog/151257/202405/151257-20240529214215231-2114462847.png" alt="image.png" loading="lazy"></p>
<blockquote>
<p>📢 所以 Decimal 值只要没有超过28~29位有效数字,就没有精度损失!是不是Very Nice!flaot、double 损失精度的根本原因是其存储机制,必须把小数转换为二进制值,再加上有限的精度位数。</p>
</blockquote>
<h2 id="32decimaldoublefloat对比">3.2、Decimal、Double、Float对比</h2>
<table>
<thead>
<tr>
<th><strong>类型</strong></th>
<th><strong>单精度 float</strong></th>
<th><strong>双精度 double</strong></th>
<th><strong>Decimal 高精度</strong>浮点数</th>
</tr>
</thead>
<tbody>
<tr>
<td>类型</td>
<td>System.Single</td>
<td>System.Double</td>
<td>System.Decimal</td>
</tr>
<tr>
<td>规范</td>
<td>IEEE754</td>
<td>IEEE754</td>
<td>无,.Net自定义类型</td>
</tr>
<tr>
<td>是否基元类型</td>
<td>是</td>
<td>是</td>
<td>是</td>
</tr>
<tr>
<td>长度</td>
<td>32位(4字节)</td>
<td>64位(8字节)</td>
<td>128位(16字节)</td>
</tr>
<tr>
<td>内部表示</td>
<td>二进制,基数为2</td>
<td>二进制,基数为2</td>
<td>十进制,基数为10</td>
</tr>
<tr>
<td>字面量(后缀)</td>
<td><code>f</code>/<code>F</code></td>
<td>后缀<code>d</code>/<code>D</code></td>
<td>后缀<code>m</code>/<code>M</code></td>
</tr>
<tr>
<td>最大精度</td>
<td>6~7</td>
<td>15~16</td>
<td>28~29位</td>
</tr>
<tr>
<td>范围</td>
<td>±3.4E38 ,<code>2^23=3.4E38</code></td>
<td>范围很大,±1.7*E308</td>
<td>-2^(96) 到 2^(96),±7.9E28</td>
</tr>
<tr>
<td>特殊值</td>
<td>+0、-0、+∞、-∞、NaN</td>
<td>+0、-0、+∞、-∞、NaN</td>
<td>无</td>
</tr>
<tr>
<td>速度</td>
<td>处理器原生支持,速度很快</td>
<td>处理器原生支持,速度很快</td>
<td>非原生支持,约<code>double</code>的<code>10%</code></td>
</tr>
</tbody>
</table>
<p>Decimal 虽然精度高,但长度也大,计算速度较慢,所以还是根据实际场景选择。财务计算一般都用 Decimal 是因为他对精度要求较高,钱不能算错,传说算错了要从程序员工资里扣😂😂。</p>
<hr>
<h1 id="04一些编程实践">04、一些编程实践</h1>
<ul>
<li>对于精度要求高的场景不适合用浮点数(double、float),推荐<code>decimal</code>,特别是价格、财务计算。</li>
<li>浮点数不适合直接相等比较,直接相等大多会出Bug。</li>
<li>在存储比较大的数字时,需注意float、double 对于整数也有精度问题。</li>
</ul>
<h2 id="41浮点数的相等比较">4.1、浮点数的相等比较</h2>
<ul>
<li>使用相同的精度进行比较,<code>Math.Round()</code>获取相同的精度值。</li>
<li>比较相似性,根据实际场景设定一个误差值,如<code>1e-8</code>,只要差值在这个误差范围内,都认为相等。</li>
</ul>
<pre><code class="language-csharp">var f1 = 0.1 + 0.2;
var f2 = 0.3;
       
Console.WriteLine(f1 == f2); //False
//相同精度
Console.WriteLine(Math.Round(f1,6) == Math.Round(f2,6)); //True
//误差范围
Console.WriteLine(Math.Abs(f1-f2)&lt;1e-8); //True
</code></pre>
<h2 id="42取整与四舍五入">4.2、取整与四舍五入</h2>
<table>
<thead>
<tr>
<th><strong>取整方式</strong></th>
<th><strong>说明/示例</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>整数相除 <code>10/4=2</code></td>
<td>抛弃余数,只留整数部分</td>
</tr>
<tr>
<td>强制转换<code>(int)2.9=2</code></td>
<td>直接截断,只留整数部分,需要注意‼️</td>
</tr>
<tr>
<td>Convert转换,四舍五入取整</td>
<td><code>Convert.ToInt32(2.7) = 3;</code> <code>Convert.ToInt32(2.2) = 2;</code></td>
</tr>
<tr>
<td>格式化截断,四射五入</td>
<td>字符串格式化时的截断,都是四舍五入, <code>$"{2.7:F0}" = "3"</code></td>
</tr>
<tr>
<td><code>Math.Ceiling()</code>,向上取整</td>
<td><code>Math.Ceiling(2.3) = 3</code>,⁉️注意负数<code>Math.Ceiling(-2.3) = -2</code></td>
</tr>
<tr>
<td><code>Math.Floor()</code>,向下取整</td>
<td><code>Math.Floor(2.3) = 2</code>,⁉️注意负数<code>Math.Floor(-2.3) = -3</code></td>
</tr>
<tr>
<td><code>Math.Truncate()</code>,截断取整</td>
<td><code>Math.Truncate(2.7) = 2</code>,只保留整数部分,同强制转换</td>
</tr>
<tr>
<td><code>Math.Round()</code>,四舍五入</td>
<td>可指定四舍五入精度,<code>Math.Round(2.77,1) = 2.8</code></td>
</tr>
</tbody>
</table>
<hr>
<h1 id="参考资料">参考资料</h1>
<ul>
<li>MSDN:System.Decimal 结构</li>
<li>MSDN:浮点数值类型(C# 引用)</li>
<li>IEEE 754-1985</li>
<li>IEEE Floating-Point Representation</li>
<li>IEEE-754 Floating Point Converter,浮点数在线转换器</li>
<li>IEEE-754 floating point numbers converter,也是一个在线浮点数在线计算器</li>
<li>IEEE754详解</li>
<li>都工作两年了,还不知道浮点数如何转二进制?</li>
</ul>
<hr>
<blockquote>
<p><strong>©️版权申明</strong>:版权所有@安木夕,本文内容仅供学习,欢迎指正、交流,转载请注明出处!<em>原文编辑地址-语雀</em></p>
</blockquote><br><br>
来源:https://www.cnblogs.com/anding/p/18221160
頁: [1]
查看完整版本: C#.Net筑基-深入解密小数内部存储的秘密