Округление чисел в 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, банковское дает четное. Смысл режима — убрать систематический перекос вверх при суммировании многих округленных значений. Это не значит, что ToEven — обязательное правило для денег: в финансах режим и момент округления задают по регламенту конкретного расчета, а не по значению по умолчанию.
Чтобы получить привычное «половина — всегда вверх», нужен режим 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 объединяет два разных класса стратегий, и это важно не спутать. ToEven и AwayFromZero округляют к ближайшему целому и различаются только на середине (дробная часть 0.5): первый уводит середину к четному, второй — от нуля; на всех прочих значениях они дают один и тот же ближайший результат. А ToZero, ToNegativeInfinity и ToPositiveInfinity — направленные: они задают направление для любого входного значения, даже если оно не стоит посередине. В .NET Core 3.0 и новее (значит, и в .NET 8) режимов пять.
В таблице ниже — два серединных входа (2.5 и -2.5) и два несерединных (2.8 и -2.8), чтобы стратегия каждого режима читалась целиком, а не только на ничьей.
| Режим | Стратегия | 2.5 |
-2.5 |
2.8 |
-2.8 |
|---|---|---|---|---|---|
ToEven |
к ближайшему | 2 | -2 | 3 | -3 |
AwayFromZero |
к ближайшему | 3 | -3 | 3 | -3 |
ToZero |
направленная (к нулю) | 2 | -2 | 2 | -2 |
ToNegativeInfinity |
направленная (вниз) | 2 | -3 | 2 | -3 |
ToPositiveInfinity |
направленная (вверх) | 3 | -2 | 3 | -2 |
Видно главное: у режимов «к ближайшему» столбцы 2.8/-2.8 совпадают (несерединное число всегда идет к ближайшему, режим тут не при делах), а у трех направленных результат на 2.8/-2.8 меняется — направление задано для любого значения, не только для середины.
Именно поэтому три направленных режима позволяют Math.Round вести себя как отдельные методы, но с округлением до нужного числа знаков: Math.Round(x, MidpointRounding.ToZero) идет к нулю, как Math.Truncate; ToNegativeInfinity — вниз, как Math.Floor; ToPositiveInfinity — вверх, как Math.Ceiling. Разница с ToEven/AwayFromZero видна не только на середине — направленный режим смещает и обычные значения:
using System;
class Program
{
static void Main()
{
double[] xs = { 2.6, -2.1 };
foreach (var x in xs)
{
double even = Math.Round(x, MidpointRounding.ToEven);
double neg = Math.Round(x, MidpointRounding.ToNegativeInfinity);
Console.WriteLine($"{x}: ToEven={even}, ToNegativeInfinity={neg}");
}
}
}
Вывод:
2.6: ToEven=3, ToNegativeInfinity=2
-2.1: ToEven=-2, ToNegativeInfinity=-3
Ни 2.6, ни -2.1 не стоят на середине, но ToNegativeInfinity все равно уводит их вниз (2 и -3), тогда как ToEven берет действительно ближайшее целое (3 и -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/float. Тип decimal хранит десятичные значения точно в пределах своей точности (до 28-29 значащих цифр), поэтому серединные случаи округляются предсказуемо. Но «точный» не значит «без округления»: при Math.Round(2.675m, 2) результат все равно округляется — просто по понятному десятичному правилу, а не по двоичному приближению. И выбор типа не заменяет бизнес-правило: режим и момент округления задают по требованиям конкретного расчета, и не стоит полагаться на значение ToEven по умолчанию, если регламент (налоги, цены, проценты, бухучет) требует другое правило.
Для одинарной точности (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 не меняет исходное число, а возвращает новое. Чтобы сохранить округленный результат, присвойте его: value = Math.Round(value, 2). Форматирование же (ToString, String.Format) меняет только строковое представление — само число остается прежним.
Выводы
- В C# округляют методами класса
Math:Round(к ближайшему),Floor(вниз, к минус бесконечности),Ceiling(вверх),Truncate(к нулю). Math.Roundпо умолчанию использует банковское округлениеToEven(к четному), а не «половина вверх»; для привычного поведения задайтеMidpointRounding.AwayFromZero.MidpointRoundingделится на два класса:ToEvenиAwayFromZeroокругляют к ближайшему и расходятся только на середине 0.5, аToZero,ToNegativeInfinityиToPositiveInfinity— направленные и задают направление для любого значения (всего в .NET 8 пять режимов).- Для денежных величин обычно берут
decimal, а неdouble/float(двоичное представление дает сюрпризы вродеMath.Round(2.675, 2) == 2.67); но тип не заменяет бизнес-правило — режим и момент округления задают по регламенту расчета. Math.Roundне меняет исходное число, а возвращает новое: чтобы сохранить результат, присвойте его (value = Math.Round(value, 2)).- Форматирование (
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.



