為什么0.1 + 0.2
不等于 0.3
?為什么16777216f
等于 16777217f
?為什么金錢計(jì)算都推薦用decimal
?本文主要學(xué)習(xí)了解一下數(shù)字背后不為人知的存儲秘密。
C#中的數(shù)字類型主要包含兩類,整數(shù)、小數(shù),C#中的小數(shù)都為浮點(diǎn)(?。?shù)。
| void Main() |
| { |
| int a1 = 100; |
| int a2 = 0x0f; |
| var b2 = 0b11; |
| var x1 = 1; |
| var y1 = 1.1; |
| Add(1, 2.3); |
| Add(1, 3); |
| } |
| private T Add<T>(T x, T y) where T : INumber<T> |
| { |
| return x + y * x; |
| } |
數(shù)值類型大多提供的成員:
🔸靜態(tài)字段 | 說明 |
---|
MaxValue | 最大值常量,Console.WriteLine(int.MaxValue); //2147483647 |
MinValue | 最小值常量 |
🔸靜態(tài)方法 | 說明 |
Parse、TryParse | 轉(zhuǎn)換為數(shù)值類型,是比較常用的類型轉(zhuǎn)換函數(shù),參數(shù)NumberStyles 可定義解析的數(shù)字格式 |
Max、Min | 比較值的大小,返回最大、小的值,int.Max(1,100) //100 |
Abs | 計(jì)算絕對值 |
IsInfinity | 是否有效值,無窮值 |
IsInteger | 是否整數(shù) |
IsNaN | 是否為NaN |
IsPositive | 是否零或正實(shí)數(shù) |
IsNegative | 是否表示負(fù)實(shí)數(shù) |
數(shù)值類型還有很多接口,如加、減、乘、除的操作符接口,作為泛型約束條件使用還是挺不錯(cuò)的。
🔸操作符接口 | 說明 |
---|
IAdditionOperators | 加法 |
ISubtractionOperators | 減法 |
IMultiplyOperators | 乘法 |
IDivisionOperators | 除法 |
| public static T Power<T>(T v1, T v2) where T : INumber<T>, |
| IMultiplyOperators<T, T, T>, IAdditionOperators<T, T, T> |
| { |
| return v1 * v1 + v2 * v2; |
| } |
C#中的小數(shù)類型有float、double、decimal 都是浮點(diǎn)數(shù),浮點(diǎn) 就是“ 浮動小數(shù)點(diǎn)位置”,小數(shù)位數(shù)不固定,小數(shù)部分、整數(shù)部分是共享數(shù)據(jù)存儲空間的。相應(yīng)的,自然也有定點(diǎn)小數(shù),固定小數(shù)位數(shù),在很多數(shù)據(jù)庫中有定點(diǎn)小數(shù),C#中并沒有。
在編碼中我們常用的浮點(diǎn)小數(shù)是float、double,經(jīng)常會遇到精度問題,以及類似下面這些面試題。
❓ 為什么0.1 + 0.2
不等于 0.3
?
❓ 為什么浮點(diǎn)數(shù)無法準(zhǔn)確的表示 0.1
?
❓ 為什么16777216f
等于 16777217f
?這里f
表示為float
。
❓ 為什么32
位float
可以最大表示3.402823E38
,64
位double
可以最大表示1.79*E308
,那么點(diǎn)位數(shù)根本存不下啊?
❓ 同樣是32位,float
的數(shù)據(jù)范圍遠(yuǎn)超int
,為什么?
| Console.WriteLine(0.1 + 0.2 == 0.3); |
| Console.WriteLine(16777216f == 16777217f); |
| Console.WriteLine(double.MaxValue); |
| Console.WriteLine(int.MaxValue); |
| Console.WriteLine(sizeof(double)); |
float、double為浮點(diǎn)數(shù),小數(shù)位數(shù)有限,比較容易損失精度。造成上面這些問題的根本原因是其存儲機(jī)制決定的,他們都遵循IEEE754格式規(guī)范,幾乎所有編程語言和處理器都支持該規(guī)范,因此大多數(shù)編程語言都有類似的問題。Decimal 為高精度浮點(diǎn)數(shù),存儲機(jī)制與float、double不同,她采用十進(jìn)制方式表示。
❗ 要搞懂float、double,就不得不了解IEEE754規(guī)范!
IEEE 754 (維基百科)是一個(gè)關(guān)于浮點(diǎn)數(shù)算術(shù)的國際標(biāo)準(zhǔn),它定義了浮點(diǎn)數(shù)的表示格式、舍入規(guī)則、特殊值、浮點(diǎn)運(yùn)算等規(guī)范。IEEE 754 標(biāo)準(zhǔn)最早發(fā)布與1985年,其中包括了四種精度規(guī)范,其中最常用的就兩種:單精度(float,4字節(jié)32位)和雙精度(double,8字節(jié)64位)。大多數(shù)編程語言、硬件處理器都支持這兩種浮點(diǎn)數(shù)據(jù)類型,因此float、double的知識幾乎是所有語言通用的,可以深入了解一下,不虧的!
IEEE 754 浮點(diǎn)數(shù)不像十進(jìn)制字面量值那樣存儲,而是用下面的二進(jìn)制方式來表示并存儲的,其實(shí)就是二進(jìn)制的科學(xué)計(jì)數(shù)法。其二進(jìn)制表示包含三個(gè)部分:符號位S、指數(shù)部分(階碼E,2為底的指數(shù))和尾數(shù)部分M。
🔸符號位(Sign):占用1位,這是浮點(diǎn)數(shù)的最高位,用于表示數(shù)字的正負(fù)。0表示正數(shù),1表示負(fù)數(shù)。
🔸指數(shù)部分(Exponent,階碼):表示為2位底的指數(shù),這里使用了移碼,實(shí)際的指數(shù)e = E-127
,這樣省去了指數(shù)的符號位,計(jì)算也更方便。
float 的指數(shù)部分8位,2^8=256
偏移量(移碼)為127,表示十進(jìn)制范圍為 [-127,128],其數(shù)據(jù)范圍就為 ±2^128
= ±3.4E38
。指數(shù)全是1即指數(shù)值為255時(shí),表示為無效數(shù)字 ±infinity或NaN。
double 的指數(shù)部分11位,2^11=2048
偏移量(移碼)為1023,十進(jìn)制值范圍[-1023,1024],因此數(shù)據(jù)范圍 ±2^1024 = ±1.79E308
。
🔸尾數(shù)部分(Mantissa):這部分表示數(shù)字的精確值(有效數(shù)字),包括整數(shù)和小數(shù)部分。尾數(shù)長度決定了精度,因?yàn)橛行?shù)字長度是有限的,因此就必然存在精度丟失的問題。
IEEE754浮點(diǎn)數(shù)都會被轉(zhuǎn)換為上述二進(jìn)制形式:**符號*尾數(shù)*2^指數(shù)**
,如 2 = 1.0 * 2^1
,0.5 = 1.0 * 2^-1
,5 = 1.25* 2^2
。數(shù)據(jù)(整數(shù)、小數(shù)部分)先轉(zhuǎn)換為二進(jìn)制形式,然后左移或右移小數(shù)點(diǎn),轉(zhuǎn)換為1.M
形式,始終都是 “1”開頭,因此就只存儲小數(shù)部分即可。
🚩浮點(diǎn)數(shù) =
十進(jìn)制 2 就表示為 2 = 1.0* 2^1
。下圖來自 在線IEEE754轉(zhuǎn)換器計(jì)算:IEEE-754 Floating Point Converter。
十進(jìn)制 0.75 表示為0.75 = 1.5* 2^-1
,指數(shù)為-1
,尾數(shù)為1.5
。
類型 | 單精度 float | 雙精度 double |
---|
CTS類型 | System.Single | System.Double |
長度 | 4字節(jié)32位 | 8字節(jié)64位 |
符號位S | 1 | 1 |
階碼(指數(shù)位T) | 8,[-127,128] | 11,[-1023,1024] |
尾數(shù)M | 23 | 52 |
階碼偏移量 | 127,e= E -127 | 1023,e= E -1023 |
精度(10進(jìn)制) | **6~7 **,2^23=8388608 | 15~16,2^52 = 4503599627370496 |
范圍 | ±3.402823E38 ,2^128=3.4E38 | ±1.79*E308,2^1024=1.79E308 |
字面量表示(后綴) | f /F | d /D |
float只能用于 表示6~7個(gè)有效數(shù)字時(shí),才不會損失精度。
| |
| Console.WriteLine(4234567f); |
| |
| Console.WriteLine(42345678f); |
| Console.WriteLine(42345671f); |
|
|
| |
| Console.WriteLine(0.2345678f); |
| |
| Console.WriteLine(2.12345678f); |
| Console.WriteLine(0.212345678f); |
對于整數(shù)轉(zhuǎn)換小數(shù)是非常容易理解的,計(jì)算機(jī)的二進(jìn)制是天然支持整數(shù)存儲為二進(jìn)制的。十進(jìn)制整數(shù)轉(zhuǎn)成二進(jìn)制通常采用 ”除 2 取余,逆序排列” 即可。
| Console.WriteLine($"{1:B4}"); |
| Console.WriteLine($"{2:B4}"); |
| Console.WriteLine($"{3:B4}"); |
| Console.WriteLine($"{4:B4}"); |
| Console.WriteLine($"{5:B4}"); |
| Console.WriteLine($"{8:B4}"); |
📢“B”格式只支持整數(shù),更多格式化參考《String字符串全面了解>字符串格式化大全》
但小數(shù)則不同,采用的是 “乘2取整法”,小數(shù)部分循環(huán)迭代,直到小數(shù)部分=0
為止。:如下0.875
的十進(jìn)制浮點(diǎn)數(shù)轉(zhuǎn)換為二進(jìn)制格式為:0.111
。
0.111
,存儲為IEE754浮點(diǎn)數(shù),轉(zhuǎn)換為1.M*2^E
結(jié)構(gòu),小數(shù)點(diǎn)右移一位,就是1.11*2^-1
。
十進(jìn)制小數(shù)6.36
轉(zhuǎn)換為二進(jìn)制,整數(shù)部分+小數(shù)部分分別轉(zhuǎn)換后合體:
二進(jìn)制無法準(zhǔn)確表示小數(shù)0.1
,是因?yàn)?code style="margin: 0px 3px; padding: 0px 5px; box-sizing: border-box; font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace, sans-serif; color: rgb(192, 52, 29); background-color: rgb(251, 229, 225); border-radius: 3px; line-height: 1.8; display: inline-block; overflow-x: auto; vertical-align: middle; border: none !important; text-indent: initial;">0.1 轉(zhuǎn)換為二進(jìn)制后是無限循環(huán)的,0.0 0011 0011 0011...
,“0011”無限循環(huán)。就像十進(jìn)制小數(shù)1/3 = 0.333
一樣。
轉(zhuǎn)換為1.M*2^E
結(jié)構(gòu),小數(shù)點(diǎn)右移4位,尾數(shù)就是1.1001 1001
,指數(shù) E = -4 +127 = 123
。
計(jì)算機(jī)存儲整數(shù)很簡單,每個(gè)數(shù)字是確定的。但小數(shù)則不同,0到1之間的小數(shù)都無限種可能,計(jì)算機(jī)有限的空間無法存儲無限的小數(shù)。因此計(jì)算機(jī)將小數(shù)也當(dāng)成“離散”的值,就像整數(shù)那樣,整數(shù)之間間隔始終為1。給小數(shù)一個(gè)間隔刻度,如下圖,用鐘表來舉例,小數(shù)刻度(步進(jìn))為0.234(十進(jìn)制)。
這樣做的好處可以兼顧“所有”小數(shù),小數(shù)的精度就取決于鐘表的“刻度”,刻度越小,精度越高,當(dāng)然存儲時(shí)所需要的空間也就越大。
因此,這個(gè)精度本質(zhì)上是由表盤間隔刻度(Gap)決定的,即使0.0012
的間隔刻度,精度達(dá)到了4位十進(jìn)制數(shù),也只能保障前2~3位小數(shù)是可靠的。0.001X、0.002X、0.003X,他始終無法表示0.0013、0.0025。
可通過提高刻度(Gap)來提高精度,但存儲長度是有限的,因此不管是那種浮點(diǎn)數(shù)都是有精度限制的。精度越高的數(shù)據(jù)類型,也需要更多的長度來存儲數(shù)據(jù)。
32位float
用了23位來存儲有效數(shù)字,十進(jìn)制也就6~7位(2^23=8388608
)。在IEEE754規(guī)范中,小數(shù)的“刻度”并不是均勻分布的,而是越來越大,數(shù)值越大則精度越低。如下面的表盤和刻度尺的示意圖,其精度(Gap)的分布是不均勻的,0
附近數(shù)字的精度最高,然后精度就越來越低了,低到超過1。
看看 float 的間隔刻度(Gap)如下圖,來自官方IEEE_754文檔:
| |
| Console.WriteLine(8388608.1f == 8388608.4f); |
| |
| Console.WriteLine(16777216f == 16777217f); |
| Console.WriteLine(16777218f == 16777219f); |
| Console.WriteLine(16777219f == 16777220f); |
下圖是double的刻度表:小于8的數(shù)字都能有16位精度。
😂 怎么感覺float很雞肋呢?限制太多了!所以編程中浮點(diǎn)數(shù)多大都用的 double 居多,float比較少。
System.Decimal 是16字節(jié)(128位)的高精度十進(jìn)制浮點(diǎn)數(shù),不同于float、double 的二進(jìn)制存儲機(jī)制,Decimal 采用10進(jìn)制存儲,表示-7.9E28 到 +7.9E28之間的十進(jìn)制數(shù)。Decimal 最大限度地減少了因舍入而導(dǎo)致的錯(cuò)誤,比較適用于對精度要求高場景,如財(cái)務(wù)計(jì)算。
📢 Decimal并不屬于IEEE754規(guī)范,也不是處理器支持的類型,計(jì)算性能要差一點(diǎn)點(diǎn)(約 double 的 10%)。
| Console.WriteLine(1f / 3f * 3f); |
| Console.WriteLine(0.1 + 0.2 == 0.3); |
| |
| Console.WriteLine(1m / 3m * 3m); |
| Console.WriteLine(0.1m + 0.2m == 0.3m); |
Decimal可以準(zhǔn)確的表示0.1
,Decimal 128位的存儲結(jié)構(gòu)如下圖(圖來源):
96位存儲一個(gè)大整數(shù),就是有效數(shù)字,Math.Pow(2,96) = 7.9E28
,最多28位有效數(shù)字,因此小數(shù)最多也就是28位(全是小數(shù)時(shí))。
剩下的32位中,有一個(gè)符號位,0 表示正數(shù),1 表示負(fù)數(shù)。其中有5
位(下圖中的第111位)表示10的指數(shù)部分(0到28的整數(shù)),可以理解為小數(shù)點(diǎn)的位置,其他位數(shù)沒有使用默認(rèn)為0(有點(diǎn)浪費(fèi)呢?)。
Decimal 表示小數(shù)其實(shí)是“障眼法”,內(nèi)部有三個(gè)int (High、Mid、Low)來表示96位有效數(shù)字,還有一個(gè)int表示指數(shù)。可以通過 decimal.GetBits()
方法獲取他們的值。下圖來自 Decimal 源碼 Decimal.cs
在Decimal中就沒有 0.1+0.2
不等于0.3
的問題,因?yàn)樗軠?zhǔn)確表示0.1
。
其根本原因就是 Decimal 不會把小數(shù)轉(zhuǎn)換為二進(jìn)制,而是就用十進(jìn)制。把小數(shù)都轉(zhuǎn)為整數(shù)存儲,如 0.1
在Decimal 中會被表示為 1* 10^-1
,尾數(shù)為1,指數(shù)為-1
,指數(shù)就是小數(shù)點(diǎn)位置。
📢 Decimal值 =
| var arr = decimal.GetBits(0.1M); |
| Console.WriteLine($"尾數(shù):{arr[2]}{arr[1]}{arr[0]}"); |
| Console.WriteLine($"指數(shù):"+$"{arr[3]:B32}".Substring(0,16)); |
| |
| |
100.1024
存儲為1001024* 10^-4
。
| var arr = decimal.GetBits(100.1024M); |
| Console.WriteLine($"尾數(shù):{arr[2]}{arr[1]}{arr[0]}"); |
| Console.WriteLine($"指數(shù):"+$"{arr[3]:B32}".Substring(0,16)); |
| |
| |
如果是負(fù)數(shù)-100.1024
,則只有符號位為1
,其他一樣
| var arr = decimal.GetBits(-100.1024M); |
| Console.WriteLine($"尾數(shù):{arr[2]}{arr[1]}{arr[0]}"); |
| Console.WriteLine($"指數(shù):"+$"{arr[3]:B32}".Substring(0,16)); |
| |
| |
📢 所以 Decimal 值只要沒有超過28~29位有效數(shù)字,就沒有精度損失!是不是Very Nice!flaot、double 損失精度的根本原因是其存儲機(jī)制,必須把小數(shù)轉(zhuǎn)換為二進(jìn)制值,再加上有限的精度位數(shù)。
類型 | 單精度 float | 雙精度 double | Decimal 高精度浮點(diǎn)數(shù) |
---|
類型 | System.Single | System.Double | System.Decimal |
規(guī)范 | IEEE754 | IEEE754 | 無,.Net自定義類型 |
是否基元類型 | 是 | 是 | 是 |
長度 | 32位(4字節(jié)) | 64位(8字節(jié)) | 128位(16字節(jié)) |
內(nèi)部表示 | 二進(jìn)制,基數(shù)為2 | 二進(jìn)制,基數(shù)為2 | 十進(jìn)制,基數(shù)為10 |
字面量(后綴) | f /F | 后綴d /D | 后綴m /M |
最大精度 | 6~7 | 15~16 | 28~29位 |
范圍 | ±3.4E38 ,2^23=3.4E38 | 范圍很大,±1.7*E308 | -2^(96) 到 2^(96),±7.9E28 |
特殊值 | +0、-0、+∞、-∞、NaN | +0、-0、+∞、-∞、NaN | 無 |
速度 | 處理器原生支持,速度很快 | 處理器原生支持,速度很快 | 非原生支持,約double 的10% |
Decimal 雖然精度高,但長度也大,計(jì)算速度較慢,所以還是根據(jù)實(shí)際場景選擇。財(cái)務(wù)計(jì)算一般都用 Decimal 是因?yàn)樗麑纫筝^高,錢不能算錯(cuò),傳說算錯(cuò)了要從程序員工資里扣😂😂。
對于精度要求高的場景不適合用浮點(diǎn)數(shù)(double、float),推薦decimal
,特別是價(jià)格、財(cái)務(wù)計(jì)算。
浮點(diǎn)數(shù)不適合直接相等比較,直接相等大多會出Bug。
在存儲比較大的數(shù)字時(shí),需注意float、double 對于整數(shù)也有精度問題。
| var f1 = 0.1 + 0.2; |
| var f2 = 0.3; |
|
|
| Console.WriteLine(f1 == f2); |
| |
| Console.WriteLine(Math.Round(f1,6) == Math.Round(f2,6)); |
| |
| Console.WriteLine(Math.Abs(f1-f2)<1e-8); |
取整方式 | 說明/示例 |
---|
整數(shù)相除 10/4=2 | 拋棄余數(shù),只留整數(shù)部分 |
強(qiáng)制轉(zhuǎn)換(int)2.9=2 | 直接截?cái)?,只留整?shù)部分,需要注意‼️ |
Convert轉(zhuǎn)換,四舍五入取整 | Convert.ToInt32(2.7) = 3; Convert.ToInt32(2.2) = 2; |
格式化截?cái)?,四射五?/td> | 字符串格式化時(shí)的截?cái)?,都是四舍五入?nbsp;$"{2.7:F0}" = "3" |
Math.Ceiling() ,向上取整 | Math.Ceiling(2.3) = 3 ,⁉️注意負(fù)數(shù)Math.Ceiling(-2.3) = -2 |
Math.Floor() ,向下取整 | Math.Floor(2.3) = 2 ,⁉️注意負(fù)數(shù)Math.Floor(-2.3) = -3 |
Math.Truncate() ,截?cái)嗳≌?/td> | Math.Truncate(2.7) = 2 ,只保留整數(shù)部分,同強(qiáng)制轉(zhuǎn)換 |
Math.Round() ,四舍五入 | 可指定四舍五入精度,Math.Round(2.77,1) = 2.8 |
轉(zhuǎn)自https://www.cnblogs.com/anding/p/18221160 作者安木夕
該文章在 2024/6/26 11:37:48 編輯過