1. Wprowadzenie
Wydaje się, że skoro komputer jest "mądry", to 0.1 + 0.2 powinno po prostu dać 0.3. Ale to nie do końca tak działa. Zobaczmy to na prostym przykładzie.
double x = 0.1;
double y = 0.2;
double sum = x + y;
Console.WriteLine(sum); // Co wypisze program?
A teraz spróbuj porównać z 0.3:
Console.WriteLine(sum == 0.3); // A tutaj będzie true czy false?
Jeśli zobaczysz False, nie zdziw się!
Powód tkwi w sposobie reprezentacji liczb w pamięci
Komputery pracują na liczbach zapisanych binarnie. Ale nie każdą dziesiętną liczbę ułamkową da się zapisać jako skończony ułamek binarny, tak samo jak 1/3 nie da się dokładnie zapisać dziesiętnie (0.333...). Na przykład 0.1 w systemie binarnym to nieskończony ułamek i trzeba go "zaokrąglić", żeby go zapisać.
Mówiąc po ludzku, double czasem "udaje", że przechowuje twoją liczbę dokładnie, ale w rzeczywistości trzyma tylko bardzo bliską wartość przybliżoną.
2. Jakie "dziwactwa" mogą się pojawić przy arytmetyce na double?
Zobaczmy praktyczne przykłady.
Przykład 1. Klasyczna "magia" 0.1 + 0.2
double a = 0.1;
double b = 0.2;
double sum = a + b;
Console.WriteLine(sum); // 0.30000000000000004
Console.WriteLine(sum == 0.3); // False
Komputer wypisał nie 0.3, tylko 0.30000000000000004. Różnica jest mała, ale jeśli zajmujesz się np. finansami — to już robi się poważne.
Przykład 2. Iteracyjne dodawanie
double result = 0;
for (int i = 0; i < 10; i++)
{
result += 0.1;
}
Console.WriteLine(result); // 0.9999999999999999
Chciałeś 1.0 — wyszło trochę mniej. Znowu, powód to zaokrąglenia wewnątrz double.
Dlaczego to ważne w prawdziwych zadaniach
Wielu myśli: "Jaka różnica, mały błąd, co z tego!" Zobaczmy przykład ze świata płatności.
Załóżmy, że twój internetowy bank sumuje 100 transakcji po 0.1 euro. Jeśli twój program "gubi" jedną stutysięczną euro na każdej iteracji, to w skali banku już "zgubisz" prawdziwe pieniądze. I wtedy od razu przyjdzie księgowy i zapyta: "Gdzie są nasze pieniądze?!"
3. Jak poprawnie porównywać liczby zmiennoprzecinkowe
Ponieważ double często nie przechowuje dokładnie tej wartości, której oczekujesz, bezpośrednie porównanie przez == może cię "wykiwać". Zamiast tego porównuje się moduł różnicy liczb z jakąś bardzo małą liczbą (epsilon).
Przykład porównania z tolerancją
double a = 0.1 + 0.2;
double b = 0.3;
double epsilon = 0.000001;
if (Math.Abs(a - b) < epsilon)
{
Console.WriteLine("Prawie równe!"); // Tak porównywać jest bezpieczniej
}
Tutaj mówimy: "Jeśli różnica między liczbami jest mniejsza niż jedna milionowa, uznajemy je za równe".
4. Specjalne wartości double: Infinity, NaN, -Infinity
Typ double przechowuje nie tylko liczby, ale też specjalne wartości. Pojawiają się one w sytuacjach, których matematyk zwykłemu studentowi kategorycznie zabrania.
Nieskończoność (Infinity)
Co się stanie, jeśli podzielisz 1 przez 0?
double result = 1.0 / 0.0;
Console.WriteLine(result); // Infinity
W C# (i wielu innych językach) dzielenie przez 0 dla double nie rzuca wyjątku! Zamiast tego wynik to specjalna wartość "dodatnia nieskończoność".
Minus nieskończoność (-Infinity)
Jeśli podzielisz liczbę ujemną przez 0, dostaniesz minus nieskończoność:
double result = -1.0 / 0.0;
Console.WriteLine(result); // -Infinity
"Nie liczba" (NaN — Not a Number)
Jeśli zrobisz coś naprawdę dziwnego, np. spróbujesz policzyć pierwiastek z liczby ujemnej:
double result = Math.Sqrt(-1);
Console.WriteLine(result); // NaN
Albo wynik dzielenia 0.0 / 0.0:
double result = 0.0 / 0.0;
Console.WriteLine(result); // NaN
NaN — to "wszystko, co nie byłoby liczbą w prawdziwym życiu".
Sprawdzanie specjalnych wartości
W C# są funkcje do sprawdzania specjalnych wartości:
Console.WriteLine(double.IsInfinity(result)); // true jeśli nieskończoność
Console.WriteLine(double.IsNaN(result)); // true jeśli NaN
Tabela: Jak double reaguje na nietypowe operacje
| Operacja | Wynik | Co jest w double |
|---|---|---|
|
Infinity | +∞ |
|
-Infinity | -∞ |
|
NaN | Nie liczba |
|
NaN | Nie liczba |
GO TO FULL VERSION