1. Giới thiệu
Ở ngoài đời hiếm khi nào tiền được trả chính xác đến một phần trăm nghìn tỷ euro, hay đo chiều dài phòng đến phần trăm milimet. Lập trình cũng vậy: kết quả làm việc với double hoặc float thường “quá lẻ”.
Đây là một vấn đề kinh điển:
double result = 10.0 / 3.0;
Console.WriteLine(result); // 3.3333333333333335
Nếu mình muốn in ra số này chỉ với hai số sau dấu phẩy (để không “dọa” người dùng) thì sao? Đó là lúc cần làm tròn.
2. Các phương pháp làm tròn cơ bản trong C#
C# có nhiều cách để làm tròn số thực. Và đây là chỗ thú vị: làm tròn cũng có nhiều kiểu! Cùng xem qua các cách chính và hiểu sự khác biệt nhé.
1) Hàm Math.Round — làm tròn kiểu truyền thống
Cách phổ biến nhất là dùng Math.Round. Nó làm tròn số đến số nguyên gần nhất (hoặc số lượng chữ số sau dấu phẩy mà bạn muốn).
Math.Round(số[, số_lượng_chữ_số[, MidpointRounding]])
- số — số cần làm tròn.
- số_lượng_chữ_số — giữ lại bao nhiêu số sau dấu phẩy (mặc định là 0, tức là làm tròn đến số nguyên).
- MidpointRounding — cách làm tròn khi gặp số lẻ 0.5 (sẽ nói kỹ hơn bên dưới).
Ví dụ:
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
Minh họa:
| Giá trị đầu vào | 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 |
Kiểu kết quả:
Tất cả các hàm trong bài này trả về kiểu double, nên nếu bạn muốn gán kết quả làm tròn thì có thể bị lỗi:
double result = 10.0 / 5.0; // Kết quả 2.0
int x = Math.Round(x); // Lỗi! Không thể gán double vào biến int như vậy
Nếu bạn muốn gán kiểu double vào biến kiểu int, bạn phải ép kiểu rõ ràng:
double result = 10.0 / 5.0; // Kết quả 2.0
int x = (int) Math.Round(x); // Ok luôn!
2) Làm tròn lên hoặc xuống
C# không chỉ có làm tròn “truyền thống”, mà còn có làm tròn ép lên hoặc ép xuống.
Làm tròn xuống (Math.Floor)
Hàm Math.Floor chỉ đơn giản là “bỏ” phần thập phân (về phía âm vô cực). Tức là luôn làm tròn xuống!
Console.WriteLine(Math.Floor(2.99)); // 2
Console.WriteLine(Math.Floor(-2.99)); // -3
Làm tròn lên (Math.Ceiling)
Hàm Math.Ceiling — ngược lại với Floor: luôn làm tròn lên (về phía dương vô cực).
Console.WriteLine(Math.Ceiling(2.01)); // 3
Console.WriteLine(Math.Ceiling(-2.01)); // -2
Bảng so sánh:
| Hàm | 2.3 | -2.3 |
|---|---|---|
| Math.Round | 2 | -2 |
| Math.Floor | 2 | -3 |
| Math.Ceiling | 3 | -2 |
3) Truncate: chỉ cắt phần thập phân
Hàm Math.Truncate — “bác sĩ phẫu thuật lạnh lùng”. Nó chỉ xóa phần thập phân, không quan tâm đến tính đúng toán học của làm tròn. Nếu x dương — kết quả như Floor, nếu âm — như Ceiling.
Console.WriteLine(Math.Truncate(2.99)); // 2
Console.WriteLine(Math.Truncate(-2.99)); // -2
3. Math.Round xử lý “số lẻ 0.5” như thế nào? (MidpointRounding)
Hãy tưởng tượng: bạn có 2.5. Làm tròn thì về đâu? Nó nằm “giữa” 2 và 3. Toán học có hai cách phổ biến:
- Làm tròn lên (“đến số lớn hơn gần nhất”)
- Làm tròn về số chẵn gần nhất (Banker's rounding)
C# mặc định dùng cách thứ hai — làm tròn về số chẵn gần nhất (“banker's rounding”, hay “làm tròn về số chẵn”).
Console.WriteLine(Math.Round(2.5)); // 2
Console.WriteLine(Math.Round(3.5)); // 4
Tại sao? Vì trong hai số nguyên cách đều, số chẵn sẽ “thắng”. Điều này giúp khi làm tròn nhiều lần sẽ không bị lệch về một phía. Rất quan trọng khi tính toán số tiền lớn trong ngân hàng (nên mới gọi là banker's rounding).
Nếu bạn muốn luôn làm tròn lên, có thể chỉ định chế độ mong muốn:
// Luôn làm tròn "ra xa số 0"
Console.WriteLine(Math.Round(2.5, 0, MidpointRounding.AwayFromZero)); // 3
Console.WriteLine(Math.Round(-2.5, 0, MidpointRounding.AwayFromZero)); // -3
Bảng chế độ MidpointRounding:
| Giá trị đầu vào | Round (Mặc định) | AwayFromZero | ToZero / ToEven |
|---|---|---|---|
| 2.5 | 2 | 3 | 2 |
| 3.5 | 4 | 4 | 4 |
| -2.5 | -2 | -3 | -2 |
4. Làm tròn đến số chữ số mong muốn: đừng quên “số 8 treo” và “số 3”!
Đôi khi cần làm tròn không phải đến số nguyên, mà ví dụ đến hai số sau dấu phẩy (ví dụ khi in tiền hoặc phần trăm).
double price = 149.9999;
double roundedPrice = Math.Round(price, 2);
Console.WriteLine(roundedPrice); // 150
Nếu mình chỉ muốn “cắt” bớt số lẻ mà không làm tròn? Ví dụ, từ 123.4567 thành 123.45.
Có thể làm bằng một trick nhỏ:
double num = 123.4567;
double result = Math.Floor(num * 100) / 100;
Console.WriteLine(result); // 123.45
Mình tự “dịch” dấu phẩy, cắt phần thập phân, rồi dịch lại.
5. Định dạng xuất ra màn hình vs làm tròn thật
Định dạng xuất ra ({x:F2}) — không phải lúc nào cũng là "làm tròn thật", mà chỉ là “mặt nạ” hiển thị số trên màn hình. Trong bộ nhớ nó vẫn là double dài ngoằng. Nếu bạn cần làm tròn thật và lưu lại, hãy dùng Math.Round.
Đây là ví dụ mà sự khác biệt có thể quan trọng:
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
// Nhưng nếu chỉ in ra với định dạng mà không làm tròn...
Console.WriteLine($"{value:F2}"); // 2.56, nhưng trong bộ nhớ vẫn là 2.555
6. Lỗi phổ biến và những điều cần chú ý
- Đừng dùng làm tròn với double cho tính toán tài chính chính xác! Với tiền trong .NET có kiểu decimal riêng — nó “không chơi” dấu phẩy động (chi tiết — ở bài sau).
- “Số 8 treo” buồn bã: do lưu trữ nhị phân nên không phải số nào cũng làm tròn “hợp lý” theo mắt thường. Ví dụ, 0.1 + 0.2 có thể không ra 0.3 đâu.
- Đừng nhầm Math.Floor với Math.Round! Floor luôn xuống, Round — theo toán học.
- Tích lũy sai số không kiểm soát: nếu bạn làm tròn liên tục trong quá trình tính toán (ví dụ trong vòng lặp), kết quả cuối có thể “trôi” bất ngờ. Thường thì chỉ nên làm tròn kết quả cuối cùng, không phải mọi bước trung gian.
7. Bảng tổng hợp: mỗi hàm cho ra gì?
| Giá trị đầu vào | 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. Fun facts
- Có lần NASA mất cả tàu vũ trụ vì làm tròn tọa độ không nghiêm ngặt và sự khác biệt giữa đơn vị mét/đế quốc. Bài học: hãy làm tròn có ý thức!
- Ngân hàng đã chuyển sang “banker's rounding” từ lâu để không mất tiền lẻ — ngày xưa ai cũng làm tròn lên, khách hàng mất từng phần cent mỗi giao dịch.
- Trong C# còn có hàm Math.Sign, trả về dấu của số — đôi khi hữu ích trước khi làm tròn, để tự quyết định “cắt” về đâu.
GO TO FULL VERSION