2013-08-01 16 views
34

Ho notato uno strano comportamento quando si moltiplicano i valori decimali in C#. Considera le seguenti operazioni di moltiplicazione:C# moltiplicazione decimale comportamento strano

1.1111111111111111111111111111m * 1m = 1.1111111111111111111111111111 // OK 
1.1111111111111111111111111111m * 2m = 2.2222222222222222222222222222 // OK 
1.1111111111111111111111111111m * 3m = 3.3333333333333333333333333333 // OK 
1.1111111111111111111111111111m * 4m = 4.4444444444444444444444444444 // OK 
1.1111111111111111111111111111m * 5m = 5.5555555555555555555555555555 // OK 
1.1111111111111111111111111111m * 6m = 6.6666666666666666666666666666 // OK 
1.1111111111111111111111111111m * 7m = 7.7777777777777777777777777777 // OK 
1.1111111111111111111111111111m * 8m = 8.888888888888888888888888889 // Why not 8.8888888888888888888888888888 ? 
1.1111111111111111111111111111m * 9m = 10.000000000000000000000000000 // Why not 9.9999999999999999999999999999 ? 

Quello che non riesco a capire sono gli ultimi due dei casi di cui sopra. Come è possibile?

+11

Benvenuti nel meraviglioso mondo degli errori di precisione. – christopher

+0

Questo non è un errore di precisione, è solo un arrotondamento. –

+0

Ecco un puzzle matematico per te: '10/9 = 1.111 ...'; '1.111 ... * 9 = 9.999 ... ≠ 10'. ;) – stakx

risposta

73

decimal memorizza 28 o 29 cifre significative (96 bit). Fondamentalmente la mantissa è nel range -/+ 79,228,162,514,264,337,593,543,950,335.

Ciò significa che fino a circa 7,9 .... è possibile ottenere 29 cifre significative con precisione, ma soprattutto che non è possibile. Ecco perché sia ​​l'8 sia il 9 vanno male, ma non i valori precedenti. Dovresti solo contare su su 28 cifre significative in generale, per evitare situazioni strane come questa.

Una volta che si riduce l'ingresso originale a 28 cifre significative, si otterrà l'output che ci si aspetta:

using System; 

class Test 
{ 
    static void Main() 
    { 
     var input = 1.111111111111111111111111111m; 
     for (int i = 1; i < 10; i++) 
     { 
      decimal output = input * (decimal) i; 
      Console.WriteLine(output); 
     } 
    } 
} 
+0

Non sono sicuro di cosa intendi esattamente per "fare affidamento su" 28 cifre. Intendi arrotondare esplicitamente cose come i risultati delle divisioni? Il fatto che 'Decimal' usi gli esponenti di base-dieci piuttosto che base-due significa che il valore numerico nominale di un' Decimal' corrisponderà a quello della sua rappresentazione concisa della stringa, ma il fatto che si tratti di un tipo a virgola mobile suggerisce l'uguaglianza -testing con 'Decimal' è adatto ad avere gli stessi problemi di qualsiasi altro tipo a virgola mobile. – supercat

+0

@supercat: Voglio dire che se hai 29 cifre da rappresentare, potresti non essere in grado di farlo esattamente, ma puoi rappresentare esattamente fino a 28 cifre. Penso che sia molto più ragionevole fare affidamento sull'uguaglianza per 'decimal' che per' float'/'double' - per prima cosa, non è necessario preoccuparsi della possibilità che alcune operazioni vengano eseguite con maggiore precisione a seconda che il il valore è in un registro o no.Dovrebbe ancora essere fatto con cura significativa, ma in molti casi penso che andrebbe bene. –

2

matematici distinguere tra numeri razionali e numeri reali superset. Le operazioni aritmetiche su numeri razionali sono ben definite e precise. L'aritmetica (usando gli operatori di addizione, sottrazione, moltiplicazione e divisione) sui numeri reali è "precisa" solo nella misura in cui i numeri irrazionali sono lasciati in una forma irrazionale (simbolica) o eventualmente convertibili in alcune espressioni in un numero razionale . Esempio, la radice quadrata di due non ha una rappresentazione decimale (o qualsiasi altra base razionale). Tuttavia, la radice quadrata di due moltiplicata per la radice quadrata di due è razionale - 2, ovviamente.

Computer, e le lingue in esecuzione su di loro, in genere implementano solo numeri razionali - nascosto dietro nomi come int, long int, float, double precisione, vero e proprio (FORTRAN) o qualche altro nome che suggerisce numeri reali. Ma i numeri razionali inclusi sono limitati, a differenza dell'insieme di numeri razionali il cui raggio è infinito.

Esempio banale: non trovato sui computer. 1/2 * 1/2 = 1/4 Funziona bene se hai una classe di numeri Rational e la dimensione dei numeratori e denominatori non supera i limiti dell'aritmetica intera. così (1,2) * (1,2) -> (1,4)

Ma se i numeri razionali disponibili erano decimali E limitati a una cifra singola dopo il decimale - non è pratico - ma rappresentativo della scelta fatta quando scegliendo un'implementazione per approssimare numeri razionali (float/reali, ecc.), quindi 1/2 sarebbe perfettamente convertibile in 0,5, quindi 0,5 + 0,5 corrisponderebbe a 1,0, ma 0,5 * 0,5 dovrà essere o 0,2 o 0,3!