1. Введение
В реальной жизни редко бывает, чтобы деньги выплачивали с точностью до сто триллионовных доли евро, а длину комнаты измеряли до сотых миллиметра. В программировании так же: результаты работы с double или float зачастую бывают “слишком дробными”.
Вот классическая проблема:
double result = 10.0 / 3.0;
Console.WriteLine(result); // 3.3333333333333335
А если мы хотим вывести это число, например, только с двумя знаками после запятой (чтобы не “пугать” пользователя)? Для этого и нужно округление.
2. Основные методы округления в C#
C# предоставляет несколько способов округлить вещественное число. И вот тут начинается интересное: округление бывает разным! Давайте рассмотрим основные варианты и поймём, чем они отличаются.
1) Функция Math.Round — классическое округление
Самый популярный способ — это метод Math.Round. Он округляет число до ближайшего целого (или нужного количества знаков после запятой).
Math.Round(число[, количество_знаков[, MidpointRounding]])
- число — что округляем.
- количество_знаков — сколько оставить знаков после запятой (по умолчанию 0, то есть до целого).
- MidpointRounding — метод округления половинок (о нём скоро поговорим).
Примеры:
double x = 2.71828;
Console.WriteLine(Math.Round(x)); // 3
Console.WriteLine(Math.Round(x, 2)); // 2.72
Console.WriteLine(Math.Round(x, 3)); // 2.718
Визуализация:
| Входное значение | Math.Round(x) | Math.Round(x, 2) |
|---|---|---|
| 2.3 | 2 | 2.3 |
| 2.5 | 2 | 2.5 |
| 2.7 | 3 | 2.7 |
| 2.71828 | 3 | 2.72 |
Тип результата:
Все функции в этой лекции возвращают тип double, поэтому если вы хотите присвоить результат округления, то можете получить ошибку:
double result = 10.0 / 5.0; // Результат 2.0
int x = Math.Round(x); // Ошибка! Нельзя просто так присвоить тип double в переменную типа int
Если вы хотите присвоить тип double в переменную типа int, то вам нужно явно указать приведение типа:
double result = 10.0 / 5.0; // Результат 2.0
int x = (int) Math.Round(x); // Все отлично работает!
2) Округление в большую или меньшую сторону
C# поддерживает не только “классическое” округление, но и принудительное округление вверх или вниз.
Округление до меньшего (Math.Floor)
Метод Math.Floor просто “отбрасывает” дробную часть (в сторону минус бесконечности). То есть всегда округляет вниз!
Console.WriteLine(Math.Floor(2.99)); // 2
Console.WriteLine(Math.Floor(-2.99)); // -3
Округление до большего (Math.Ceiling)
Метод Math.Ceiling — противоположность Floor: он всегда округляет вверх (в сторону плюс бесконечности).
Console.WriteLine(Math.Ceiling(2.01)); // 3
Console.WriteLine(Math.Ceiling(-2.01)); // -2
Сравнительная схема:
| Функция | 2.3 | -2.3 |
|---|---|---|
| Math.Round | 2 | -2 |
| Math.Floor | 2 | -3 |
| Math.Ceiling | 3 | -2 |
3) Truncate: просто обрезать дробную часть
Метод Math.Truncate — “спокойный хирург”. Он просто удаляет дробную часть, не заботясь о математической корректности с точки зрения округления. Если x положительное — результат как у Floor, если отрицательное — как у Ceiling.
Console.WriteLine(Math.Truncate(2.99)); // 2
Console.WriteLine(Math.Truncate(-2.99)); // -2
3. Как работает Math.Round для “половинок”? (MidpointRounding)
Представьте: у вас есть 2.5. Нужно округлять — но куда? Вроде бы “поровну” между 2 и 3. В математике есть два популярных подхода:
- Округление в большую сторону (“до ближайшего большего”)
- Округление к ближайшему чётному (Banker's rounding)
C# по умолчанию использует второй подход — округление к ближайшему чётному (“banker's rounding”, или “округление в сторону чётного”).
Console.WriteLine(Math.Round(2.5)); // 2
Console.WriteLine(Math.Round(3.5)); // 4
Почему? Потому что из двух равноудалённых целых чётное “выигрывает”. Это сделано, чтобы при большом количестве округлений не было систематической ошибки в сторону увеличения или уменьшения. Это очень важно, например, при подсчёте больших сумм денег в банке (отсюда и название).
Если вы хотите всегда округлять вверх, можно явно указать желаемый режим:
// Округлить всегда "от нуля"
Console.WriteLine(Math.Round(2.5, 0, MidpointRounding.AwayFromZero)); // 3
Console.WriteLine(Math.Round(-2.5, 0, MidpointRounding.AwayFromZero)); // -3
Таблица режимов MidpointRounding:
| Входное значение | Round (По умолчанию) | AwayFromZero | ToZero / ToEven |
|---|---|---|---|
| 2.5 | 2 | 3 | 2 |
| 3.5 | 4 | 4 | 4 |
| -2.5 | -2 | -3 | -2 |
4. Округление до нужного количества знаков: не забывайте про "восьмерки" и "тройки"!
Иногда нужно округлить не до целого, а, скажем, до двух знаков после запятой (например, для вывода денег или процентов).
double price = 149.9999;
double roundedPrice = Math.Round(price, 2);
Console.WriteLine(roundedPrice); // 150
Что, если мы хотим просто “отбросить” лишние знаки, но не округлять? Например, из 123.4567 сделать 123.45.
Так можно сделать с помощью маленького трюка:
double num = 123.4567;
double result = Math.Floor(num * 100) / 100;
Console.WriteLine(result); // 123.45
Мы вручную “сдвигаем” запятую, отрезаем дробную часть, и сдвигаем обратно.
5. Форматирование результата вывода vs округление
Форматирование вывода ({x:F2}) — это не всегда "настоящее" округление, а только “маска”, как выглядит число на экране. В памяти оно так и останется длинным “хвостатым” double. Если вам нужно именно округлить значение и сохранить его, используйте Math.Round.
Вот пример, где разница может быть критична:
double value = 2.555;
Console.WriteLine($"{value:F2}"); // 2.56
Console.WriteLine(Math.Round(value, 2)); // 2.56
double stored = Math.Round(value, 2);
Console.WriteLine(stored); // 2.56
// Но если просто вывести без округления, но с форматом...
Console.WriteLine($"{value:F2}"); // 2.56, но в памяти — 2.555
6. Типичные ошибки и нюансы
- Не используйте округление для точных финансовых расчётов с double! Для денег в .NET есть специальный тип decimal — он “не балуется” плавающей запятой (подробнее — в одной из следующих лекций).
- Печальные “зависшие восьмёрки”: из-за двоичного хранения не все числа округляются “логично” на глаз. Например, 0.1 + 0.2 может быть совсем не 0.3.
- Не путайте Math.Floor и Math.Round! Первый всегда вниз, второй — как математика велит.
- Неконтролируемое накопление погрешности: если вы часто округляете результат вычислений по пути (например, внутри цикла), итоговое значение может неожиданно “уплыть”. Обычно лучше округлять только конечный результат, а не все промежуточные шаги.
7. Финальная таблица: какой метод что даёт?
| Входное значение | Math.Round(x) | Math.Floor(x) | Math.Ceiling(x) | Math.Truncate(x) |
|---|---|---|---|---|
| 3.2 | 3 | 3 | 4 | 3 |
| 3.5 | 4 | 3 | 4 | 3 |
| -3.2 | -3 | -4 | -3 | -3 |
| -3.5 | -4 | -4 | -3 | -3 |
8. Забавные факты
- Однажды NASA потеряла космический аппарат из-за нестрогого округления координат и разницы между метрическими/имперскими единицами. Мораль: округляйте осознанно!
- Банки давно перешли на “banker's rounding”, чтобы не терять копейки — раньше по “старинке” все округляли вверх, и клиенты теряли доли цента на каждом переводе.
- В C# есть даже метод Math.Sign, который возвращает знак числа — иногда полезно перед округлением, чтобы самому выбирать, куда “отрубать”.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ