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, який повертає знак числа — іноді корисно перед округленням, щоб самостійно обирати, куди «відрубати».
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ