Округление чисел в C#: Math.Round, MidpointRounding и decimal

Округление чисел в C#: Math.Round, MidpointRounding и decimal Полезное

Округление чисел в C# — это приведение значения с дробной частью к меньшей точности с помощью методов класса System.Math: Math.Round (к ближайшему), Math.Floor (вниз), Math.Ceiling (вверх) и Math.Truncate (отбросить дробную часть). Главная особенность, которая удивляет новичков: Math.Round по умолчанию округляет не «школьным» способом, а по банковскому правилу — к ближайшему четному.

Ниже разберем все четыре метода, поведение Math.Round на серединном значении, полный набор режимов MidpointRounding, округление до заданного числа знаков и важную разницу между double/float и decimal. Примеры даны для .NET 8; вывод указан по спецификации методов.

Четыре способа округлить число

Сначала — карта методов. Все они статические, лежат в классе Math и имеют перегрузки для double и decimalMath.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.

OTUS Журнал
Скидка 5% 14-20 сентября на курсы (popup)