1. Введение
Кажется, что если компьютер "умный", то 0.1 + 0.2 должно быть просто 0.3. Но это не совсем так. Давайте разберёмся на простом примере.
double x = 0.1;
double y = 0.2;
double sum = x + y;
Console.WriteLine(sum); // Что выведет программа?
А теперь попробуйте сравнить с 0.3:
Console.WriteLine(sum == 0.3); // А тут будет true или false?
Если вы увидите False, не удивляйтесь!
Причина кроется в представлении чисел в памяти
Компьютеры работают с числами в двоичной системе. Но далеко не все десятичные дроби можно представить конечной двоичной дробью, так же как 1/3 нельзя точно записать десятичной дробью (0.333...). Например, 0.1 в двоичной системе — это бесконечная дробь, и её приходится "округлять" для хранения.
Если перевести это на бытовой язык, то double иногда "делает вид", что хранит ваше число точно, но на самом деле хранит лишь очень близкое приближенное значение.
2. Какие "глюки" бывают при арифметике с double?
Давайте рассмотрим практические примеры.
Пример 1. Классическая "магия" 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
Компьютер вывел не 0.3, а 0.30000000000000004. Разница маленькая, но если вы занимаетесь, например, финансами — это уже критично.
Пример 2. Итеративное сложение
double result = 0;
for (int i = 0; i < 10; i++)
{
result += 0.1;
}
Console.WriteLine(result); // 0.9999999999999999
Хотели 1.0 — получили чуть меньше. Снова, причина в округлении внутри double.
Почему это важно в реальных задачах
Многие думают: "Какая разница, ну небольшая ошибка, подумаешь!" Рассмотрим пример из мира платежей.
Допустим, ваш интернет-банк складывает 100 транзакций по 0.1 евро. Если ваша программа "теряет" по одной стотысячной евро на каждую итерацию, то на масштабе банка вы уже "потеряете" реальные деньги. Вот тут к вам сразу придёт бухгалтер и спросит: "Где наши деньги?!"
3. Как правильно сравнивать вещественные числа
Поскольку double часто не может хранить именно то значение, которое вы ожидаете, прямое сравнение через == может "подвести". Вместо этого принято сравнивать модуль разности чисел с некоторым очень маленьким числом (epsilon).
Пример сравнения с допуском
double a = 0.1 + 0.2;
double b = 0.3;
double epsilon = 0.000001;
if (Math.Abs(a - b) < epsilon)
{
Console.WriteLine("Почти равно!"); // Так сравнивать безопаснее
}
Здесь мы говорим: "Если разница между числами меньше, чем одна миллионная, считаем, что числа равны".
4. Специальные значения double: Infinity, NaN, -Infinity
Тип double хранит не только числа, но и особые значения. Они возникают в ситуациях, которые математик обычному студенту строго-настрого запрещает.
Бесконечность (Infinity)
Что будет, если поделить 1 на 0?
double result = 1.0 / 0.0;
Console.WriteLine(result); // Infinity
В C# (и многих языках) деление на 0 для double не вызывает исключения! Вместо этого результат становится специальным значением "положительная бесконечность".
Минус бесконечность (-Infinity)
Если поделить отрицательное число на 0, то вы получите отрицательную бесконечность:
double result = -1.0 / 0.0;
Console.WriteLine(result); // -Infinity
"Не число" (NaN — Not a Number)
Если сделать что-то совсем странное, например, попытаться вычислить корень из отрицательного числа:
double result = Math.Sqrt(-1);
Console.WriteLine(result); // NaN
Или результат деления 0.0 / 0.0:
double result = 0.0 / 0.0;
Console.WriteLine(result); // NaN
NaN — это "все, что не являлось бы числом в реальной жизни".
Проверка на специальные значения
В C# есть функции для проверки специальных значений:
Console.WriteLine(double.IsInfinity(result)); // true если бесконечность
Console.WriteLine(double.IsNaN(result)); // true если NaN
Таблица: Как double реагирует на необычные операции
| Операция | Результат | Что хранится в double |
|---|---|---|
|
Infinity | +∞ |
|
-Infinity | -∞ |
|
NaN | Не число |
|
NaN | Не число |
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ