Округление чисел в C# — это приведение значения с дробной частью к меньшей точности с помощью методов класса System.Math: Math.Round (к ближайшему), Math.Floor (вниз), Math.Ceiling (вверх) и Math.Truncate (отбросить дробную часть). Главная особенность, которая удивляет новичков: Math.Round по умолчанию округляет не «школьным» способом, а по банковскому правилу — к ближайшему четному.
Содержание
Ниже разберем все четыре метода, поведение Math.Round на серединном значении, полный набор режимов MidpointRounding, округление до заданного числа знаков и важную разницу между double/float и decimal. Примеры даны для .NET 8; вывод указан по спецификации методов.
Четыре способа округлить число
Сначала — карта методов. Все они статические, лежат в классе Math и имеют перегрузки для double и decimal (а Math.Round — еще и с указанием числа знаков).
| Метод | Что делает | Направление |
|---|---|---|
Math.Round |
к ближайшему целому или к N знакам | к ближайшему, ничью решает MidpointRounding |
Math.Floor |
вниз | к минус бесконечности |
Math.Ceiling |
вверх | к плюс бесконечности |
Math.Truncate |
отбрасывает дробную часть | к нулю |
Разница между Truncate, Floor и Ceiling видна только на отрицательных числах: Truncate идет к нулю, Floor — всегда вниз (к минус бесконечности), Ceiling — всегда вверх. На положительных Truncate и Floor совпадают.
Math.Round и банковское округление
Первое, что стоит запомнить: Math.Round без дополнительных параметров использует режим MidpointRounding.ToEven — банковское округление (banker’s rounding). Когда значение стоит ровно посередине между двумя целыми (дробная часть равна 0.5), метод выбирает не «вверх», а ближайшее четное число.
using System;
class Program
{
static void Main()
{
Console.WriteLine(Math.Round(0.5)); // 0.5 -> ближайшее четное 0
Console.WriteLine(Math.Round(1.5)); // 1.5 -> 2
Console.WriteLine(Math.Round(2.5)); // 2.5 -> ближайшее четное 2
Console.WriteLine(Math.Round(3.5)); // 3.5 -> 4
Console.WriteLine(Math.Round(2.4)); // не середина -> 2
Console.WriteLine(Math.Round(2.6)); // не середина -> 3
}
}
Вывод:
0
2
2
4
2
3
Обратите внимание на пары 0.5 -> 0 и 2.5 -> 2: там, где школьное правило дало бы 1 и 3, банковское дает четное. Смысл режима — убрать систематический перекос вверх при суммировании многих округленных значений, поэтому его и применяют в финансовых расчетах.
Чтобы получить привычное «половина — всегда вверх», нужен режим MidpointRounding.AwayFromZero (от нуля). Сравним оба режима на одних и тех же числах.
using System;
class Program
{
static void Main()
{
double[] xs = { 0.5, 1.5, 2.5, 3.5 };
foreach (var x in xs)
{
double even = Math.Round(x, MidpointRounding.ToEven);
double away = Math.Round(x, MidpointRounding.AwayFromZero);
Console.WriteLine($"{x}: ToEven={even}, AwayFromZero={away}");
}
}
}
Вывод:
0.5: ToEven=0, AwayFromZero=1
1.5: ToEven=2, AwayFromZero=2
2.5: ToEven=2, AwayFromZero=3
3.5: ToEven=4, AwayFromZero=4
Все режимы MidpointRounding
MidpointRounding описывает только одно: как разрешать ничью, когда значение стоит ровно на середине. На остальные значения режим не влияет — там всегда берется действительно ближайшее число. В .NET Core 3.0 и новее (значит, и в .NET 8) режимов пять.
| Режим | Как решает ничью 0.5 | Math.Round(2.5) |
Math.Round(-2.5) |
|---|---|---|---|
ToEven |
к ближайшему четному | 2 | -2 |
AwayFromZero |
от нуля | 3 | -3 |
ToZero |
к нулю | 2 | -2 |
ToNegativeInfinity |
вниз | 2 | -3 |
ToPositiveInfinity |
вверх | 3 | -2 |
Важно не путать эти режимы с методами Truncate, Floor и Ceiling. Режим MidpointRounding.ToZero не то же самое, что Math.Truncate: ToZero срабатывает только для ровной середины, а Truncate отбрасывает дробную часть у любого числа. Например, Math.Round(2.4, MidpointRounding.ToNegativeInfinity) даст 2, потому что 2.4 — не середина и округляется к ближайшему; а Math.Floor(2.4) тоже даст 2, но по другой причине — оно всегда идет вниз.
Округление до знаков после запятой
У Math.Round есть перегрузка со вторым аргументом — число знаков после запятой (для double допустимо от 0 до 15, для decimal — от 0 до 28). Правило серединного значения работает так же и настраивается третьим аргументом.
Чтобы результат был предсказуем на серединных значениях, примеры дам на decimal: этот тип хранит числа в десятичной системе, поэтому 2.345m — это ровно 2.345, а не приближение.
using System;
class Program
{
static void Main()
{
Console.WriteLine(Math.Round(2.345m, 2)); // ToEven: 2.34 или 2.35 -> четное 2.34
Console.WriteLine(Math.Round(2.355m, 2)); // ToEven: 2.35 или 2.36 -> четное 2.36
Console.WriteLine(Math.Round(2.345m, 2, MidpointRounding.AwayFromZero)); // 2.35
}
}
Вывод:
2.34
2.36
2.35
double, float и decimal: откуда берется 2.67 вместо 2.68
Здесь кроется самая частая ошибка. Типы float и double хранят числа в двоичной системе, и многие «круглые» десятичные дроби в ней не представимы точно. Значение 2.675 в double на самом деле хранится как чуть меньшее число, поэтому при округлении до двух знаков оно уходит вниз — в 2.67, а не в ожидаемые 2.68.
using System;
class Program
{
static void Main()
{
double d = 2.675;
decimal m = 2.675m;
Console.WriteLine(Math.Round(d, 2)); // double: хранится как ~2.67499..., -> 2.67
Console.WriteLine(Math.Round(m, 2)); // decimal: ровно 2.675, ToEven -> 2.68
}
}
Вывод:
2.67
2.68
Отсюда практическое правило: для денег и любых расчетов, где важна десятичная точность, берите decimal, а не double. Тип decimal хранит десятичные значения точно в пределах своей точности (до 28-29 значащих цифр), поэтому серединные случаи округляются предсказуемо. Но «точный» не значит «без округления»: при Math.Round(2.675m, 2) результат все равно округляется — просто по понятному десятичному правилу, а не по двоичному приближению.
Для одинарной точности (float) есть отдельный класс MathF с методами MathF.Round, MathF.Floor и другими — он избавляет от лишних преобразований float в double и обратно.
Floor, Ceiling и Truncate
Эти три метода округляют без вариантов «к ближайшему» — направление у каждого фиксировано, и разница видна на знаке числа.
using System;
class Program
{
static void Main()
{
Console.WriteLine(Math.Floor(2.7)); // вниз -> 2
Console.WriteLine(Math.Floor(-2.1)); // вниз (к -inf) -> -3
Console.WriteLine(Math.Ceiling(2.1)); // вверх -> 3
Console.WriteLine(Math.Ceiling(-2.9)); // вверх (к +inf) -> -2
Console.WriteLine(Math.Truncate(2.7)); // к нулю -> 2
Console.WriteLine(Math.Truncate(-2.7)); // к нулю -> -2
}
}
Вывод:
2
-3
3
-2
2
-2
Сравните Math.Floor(-2.1) и Math.Truncate(-2.7): Floor ушел к -3 (вниз по числовой оси), а Truncate оставил -2 (просто отбросил дробную часть). Это и есть разница между «к минус бесконечности» и «к нулю».
Форматирование строкой: это не округление числа
Часто округление нужно только для вывода — показать цену с двумя знаками. Тогда удобнее не менять число, а отформатировать его при выводе через ToString или String.Format. Важно понимать: результат такого форматирования — строка, а само число не меняется.
using System;
using System.Globalization;
class Program
{
static void Main()
{
var c = CultureInfo.InvariantCulture;
Console.WriteLine((3.14159).ToString("F2", c)); // 3.14
Console.WriteLine((1234.5).ToString("N2", c)); // 1,234.50
Console.WriteLine(string.Format(c, "{0:F2}", 3.14159)); // 3.14
}
}
Вывод:
3.14
1,234.50
3.14
Спецификатор F2 дает фиксированную точку с двумя знаками, N2 добавляет разделители групп разрядов. Разделитель дробной части зависит от культуры: в русской локали (ru-RU) это запятая, а не точка, поэтому в примере явно задана InvariantCulture, чтобы вывод был предсказуемым. Если нужно именно изменить хранимое значение, а не его вид на экране, используйте Math.Round, а форматирование оставьте для отображения.
Выводы
- В C# округляют методами класса
Math:Round(к ближайшему),Floor(вниз, к минус бесконечности),Ceiling(вверх),Truncate(к нулю). Math.Roundпо умолчанию использует банковское округлениеToEven(к четному), а не «половина вверх»; для привычного поведения задайтеMidpointRounding.AwayFromZero.MidpointRoundingменяет только разрешение ничьей 0.5; в .NET 8 доступны пять режимов, включаяToZero,ToNegativeInfinityиToPositiveInfinity.- Для денег берите
decimal, а неdouble/float: двоичное представление дает сюрпризы вродеMath.Round(2.675, 2) == 2.67. - Форматирование (
ToString("F2"),String.Format) возвращает строку и зависит от культуры — это вывод, а не изменение числа.
Где применяется
Округление в C# нужно почти в любой прикладной задаче: расчет цен и налогов, формирование чеков и отчетов, отображение метрик и процентов, подготовка данных для графиков. Выбор метода и режима напрямую влияет на корректность денег: банковское округление в бухгалтерии, AwayFromZero там, где половину принято поднимать, decimal вместо double для сумм.
Освойте тему на практике
Разобраться с типами данных, методами Math и обработкой чисел на практике помогает курс C#-разработчик. Базовый уровень. Посмотреть формат занятий и уровень до старта можно на бесплатных открытых уроках Otus — это живые занятия с преподавателями.
FAQ
Почему Math.Round(2.5) возвращает 2, а не 3? Потому что по умолчанию работает банковское округление MidpointRounding.ToEven: серединное значение уходит к ближайшему четному. Чтобы получить 3, вызовите Math.Round(2.5, MidpointRounding.AwayFromZero).
Как округлить вверх всегда, независимо от дробной части? Используйте Math.Ceiling — он поднимает любое число к следующему целому в сторону плюс бесконечности; для положительных это округление вверх. Для округления вниз есть Math.Floor.
Чем Math.Truncate отличается от приведения (int)? Оба отбрасывают дробную часть в сторону нуля, но Math.Truncate возвращает тот же тип (double или decimal) и работает с большими значениями, а приведение (int) дает целочисленный тип и переполнится, если число не помещается в диапазон int.



