Danh sách bài viết

Bài 34: Floating-Point: f32 vs f64

Bài 34 của series Rust Cơ Bản — phân biệt f32 (IEEE-754 single precision 32-bit) và f64 (double precision 64-bit, mặc định trong Rust), giải thích vì sao 0.1 + 0.2 != 0.3, xử lý NaN / Infinity / negative zero, vì sao float chỉ có PartialEq chứ không có Eq, idiom so sánh equality an toàn qua EPSILON, các method phổ biến (sqrt, powf, sin, floor), và rule of thumb chọn f32 vs f64 cho từng use case.

09/06/2026
11 phút đọc
0 lượt xem
1

Mục Tiêu Bài Học

Sau bài học, bạn sẽ:

  • Phân biệt rõ f32 (IEEE-754 single precision, 32-bit, ~7 chữ số chính xác) và f64 (double precision, 64-bit, ~15-17 chữ số), biết vì sao f64default trong Rust.
  • Hiểu nhanh cấu trúc IEEE-754 (sign + exponent + mantissa) đủ để giải thích vì sao 0.1 + 0.2 != 0.3 trong mọi ngôn ngữ dùng float chuẩn.
  • Biết các giá trị đặc biệt: NaN, INFINITY, NEG_INFINITY, negative zero — và cách check qua is_nan(), is_infinite(), is_finite().
  • Hiểu vì sao float chỉ implement PartialEqkhông implement Eq (NaN vi phạm reflexive axiom) — hệ quả: không dùng được f64 làm key cho HashMap.
  • Nắm idiom so sánh equality an toàn: (a - b).abs() < EPSILON với epsilon thực tế cho f32 / f64.
  • Biết các method phổ biến: sqrt, powi/powf, sin/cos, ln/log10, floor/ceil/round, abs, min/max.
  • Biết rule of thumb chọn f32 vs f64 cho graphics, ML, scientific, financial — và tại sao tuyệt đối không dùng float cho tiền tệ.
2

Tổng Quan f32 vs f64

Rust chỉ có 2 loại floating-point, cả hai đều theo chuẩn IEEE-754 binary format:

  • f32 — single precision, 32-bit (4 byte).
  • f64 — double precision, 64-bit (8 byte). Đây là type mặc định khi bạn viết literal 1.0 không có suffix.

Bảng so sánh chi tiết:

Khía cạnh f32 f64
Bit layout 32 (1 sign + 8 exp + 23 mantissa) 64 (1 sign + 11 exp + 52 mantissa)
Precision ~7 chữ số thập phân ~15-17 chữ số thập phân
Range tuyệt đối ±~3.4 × 1038 ±~1.8 × 10308
Smallest positive ~1.2 × 10-38 ~2.2 × 10-308
Default trong Rust KHÔNG CÓ (literal 1.0f64)
Use case chính graphics vertex, ML fp32/fp16, GPU compute scientific, default cho mọi calculation thông thường

Vì sao f64 là mặc định mà không phải f32 (khác C, nơi float mặc định là 32-bit)? Lý do thực tế: CPU 64-bit hiện đại (x86-64, arm64) thao tác f64 nhanh ngang f32 nhờ FPU rộng 64-80 bit; precision f64 đủ cho 99% bài toán; chọn f32 mặc định dễ gây bug rounding bất ngờ. Rust ưu tiên correctness — bạn phải chủ động chọn f32 khi thực sự cần.

3

IEEE-754 Refresher 60 Giây

Hiểu nhanh IEEE-754 là chìa khoá để không "ngơ ngác" khi gặp các pitfall float kinh điển. Mỗi giá trị float gồm 3 phần:

  • Sign bit (1 bit) — dương hay âm.
  • Exponent (8 bit cho f32, 11 bit cho f64) — số mũ cơ số 2, có bias để biểu diễn cả âm.
  • Mantissa (23 bit cho f32, 52 bit cho f64) — phần fractional, gồm implicit leading 1.

Công thức biểu diễn: value = (-1)^sign × 1.mantissa × 2^(exponent - bias).

Hệ quả quan trọng nhất: float chỉ biểu diễn chính xác các số có dạng m / 2^n (m, n nguyên). Các phân số khác phải làm tròn. Ví dụ:

  • 0.5 = 1/2 → chính xác (2-1).
  • 0.25 = 1/4 → chính xác (2-2).
  • 0.125 = 1/8 → chính xác.
  • 0.1 = 1/10KHÔNG chính xác. Trong binary, 0.1 là một chuỗi vô hạn tuần hoàn 0.0001100110011..., bị cắt cụt khi lưu vào 23 hoặc 52 bit mantissa.
  • 0.2 = 2/10 → cũng không chính xác (cùng lý do).

0.10.2 được lưu xấp xỉ (xấp xỉ trên), tổng của chúng cũng xấp xỉ — và xấp xỉ đó không khớp với xấp xỉ của 0.3 (xấp xỉ dưới). Demo:

fn main() {
    let a = 0.1_f64 + 0.2;
    let b = 0.3_f64;

    println!("a = {a}");                // 0.30000000000000004
    println!("b = {b}");                // 0.3
    println!("{a} == {b} ? {}", a == b); // false!

    // Xem chi tiết 20 chữ số sau dấu phẩy
    println!("a = {a:.20}");  // 0.30000000000000004441
    println!("b = {b:.20}");  // 0.29999999999999998890
}

Đây không phải bug của Rust — JavaScript, Python, Java, C/C++, Go đều cho kết quả y hệt vì cùng dùng IEEE-754. Cách khắc phục: tránh so sánh equality trực tiếp với float (xem mục 7), hoặc dùng decimal type cho domain đòi hỏi chính xác tuyệt đối (financial).

4

Literal Syntax Cho Float

Rust linh hoạt trong cách viết float literal:

fn main() {
    // Default: literal có dấu chấm là f64
    let x = 1.0;          // type được infer là f64
    let y = 3.14;         // f64
    let z = 2.5;          // f64

    // Ép sang f32 bằng suffix
    let a = 1.0_f32;      // f32 rõ ràng
    let b: f32 = 2.5;     // annotation cũng được, suffix không bắt buộc
    let c = 2.5f32;       // không underscore cũng OK nhưng đọc khó hơn

    // Scientific notation - phổ biến cho số rất lớn / rất nhỏ
    let kilo = 1e3;       // = 1000.0 (f64)
    let milli = 1e-3;     // = 0.001
    let very_small = 2.5e-4;   // = 0.00025
    let avogadro = 6.022e23;   // = 6.022 × 10^23

    // Underscore làm separator cho dễ đọc (giống integer)
    let big = 1_000_000.5;     // = 1000000.5
    let pi_long = 3.141_592_653_589_793_f64;

    // KHÔNG hợp lệ: thiếu phần fraction sau dấu chấm
    // let bad = 5.;       // compile error - phải viết 5.0
    // Cách viết 5 như float: 5.0 hoặc 5_f64

    // Convert từ integer literal sang float
    let n: f64 = 42 as f64;       // cast tường minh qua `as`
    let m = 42.0_f64;             // hoặc viết thẳng là float literal

    println!("x={x}, kilo={kilo}, milli={milli}, big={big}");
    println!("avogadro = {avogadro:e}");  // 6.022e23
}

Lưu ý nhỏ: literal 5. (không có gì sau dấu chấm) không hợp lệ trong Rust — phải viết 5.0. Quy tắc này tránh nhầm lẫn với method call (vd 5.pow(2)).

5

NaN, Infinity, Negative Zero

IEEE-754 dành riêng vài bit pattern đặc biệt để biểu diễn các giá trị "không phải số bình thường":

  • NaN (Not a Number) — kết quả của các phép tính vô nghĩa như 0.0 / 0.0, sqrt(-1.0), inf - inf.
  • INFINITY — vượt range trên hoặc chia một số dương cho 0.
  • NEG_INFINITY — vượt range dưới hoặc chia một số âm cho 0.
  • Negative zero (-0.0) — kết quả của underflow âm; bằng +0.0 qua == nhưng bit pattern khác.
fn main() {
    // Constant trên f64 (tương tự cho f32)
    let nan = f64::NAN;
    let inf = f64::INFINITY;
    let neg_inf = f64::NEG_INFINITY;
    let pos_zero = 0.0_f64;
    let neg_zero = -0.0_f64;

    // Sinh NaN / Infinity từ phép toán
    let nan_from_zero = 0.0_f64 / 0.0;        // NaN
    let inf_from_div = 1.0_f64 / 0.0;         // INFINITY
    let neg_inf_from_div = -1.0_f64 / 0.0;    // NEG_INFINITY
    let nan_from_sqrt = (-1.0_f64).sqrt();    // NaN
    let nan_from_log = (-2.0_f64).ln();       // NaN

    // Check qua các predicate method
    println!("is_nan: {}", nan.is_nan());                  // true
    println!("is_infinite: {}", inf.is_infinite());        // true
    println!("is_finite: {}", inf.is_finite());            // false
    println!("is_finite (3.14): {}", 3.14_f64.is_finite()); // true
    println!("is_sign_negative: {}", neg_zero.is_sign_negative()); // true

    // Negative zero == positive zero theo IEEE-754
    println!("-0.0 == 0.0 ? {}", neg_zero == pos_zero);    // true
    // Nhưng dấu khác - chia phát hiện ra
    println!("1.0 / -0.0 = {}", 1.0 / neg_zero);           // -inf
    println!("1.0 /  0.0 = {}", 1.0 / pos_zero);           // inf

    // CHÚ Ý: NaN không bao giờ bằng chính nó
    println!("NaN == NaN ? {}", nan == nan);  // false (!)
    println!("NaN != NaN ? {}", nan != nan);  // true (!)

    // NaN còn "lây" - mọi phép toán có NaN đều ra NaN
    let result = nan + 1.0 * 2.0 - 5.0;
    println!("NaN trong phép tính: {} (is_nan: {})",
             result, result.is_nan());

    // Idiom kiểm tra valid: dùng is_finite() để loại cả NaN và Inf
    let user_input = 1.0_f64 / 0.0;
    if !user_input.is_finite() {
        println!("Giá trị không hợp lệ, từ chối xử lý");
    }
}

Quy tắc thực tế: trước khi dùng kết quả float từ phép tính có rủi ro (chia, sqrt, log, exp), luôn check is_finite() nếu là user input hoặc data từ ngoài. Trong tính toán nội bộ controlled, có thể bỏ qua nếu chắc chắn không sinh NaN.

6

Không Có Eq Trait — Chỉ PartialEq

Đây là điểm khác biệt rất quan trọng giữa float và integer trong Rust:

  • Integer implement cả PartialEqEq — quan hệ bằng "đầy đủ", có 3 tính chất: reflexive (a == a luôn đúng), symmetric (a == b ⇔ b == a), transitive (a == b ∧ b == c ⇒ a == c).
  • Float chỉ implement PartialEq — quan hệ "một phần" vì vi phạm reflexive: NaN == NaNfalse. Theo IEEE-754, NaN không bằng bất cứ thứ gì, kể cả chính nó.

Hệ quả thực tế: bất cứ trait nào yêu cầu Eq đều không dùng được với float trực tiếp. Ví dụ điển hình là HashMap<K, V> — key phải implement Hash + Eq:

use std::collections::HashMap;

fn main() {
    // KHÔNG compile - f64 không implement Eq, kéo theo không implement Hash
    // let mut map: HashMap<f64, &str> = HashMap::new();
    // map.insert(3.14, "pi");
    //
    // error[E0277]: the trait bound `f64: Eq` is not satisfied
    // error[E0277]: the trait bound `f64: Hash` is not satisfied

    // Workaround 1: dùng bit representation qua to_bits() - u64 có Eq + Hash
    let mut bits_map: HashMap<u64, &str> = HashMap::new();
    bits_map.insert(3.14_f64.to_bits(), "pi");
    bits_map.insert(2.71_f64.to_bits(), "e");

    let key = 3.14_f64.to_bits();
    println!("{:?}", bits_map.get(&key));  // Some("pi")

    // Workaround 2: crate `ordered-float` cung cấp OrderedFloat<f64>
    // use ordered_float::OrderedFloat;
    // let mut map: HashMap<OrderedFloat<f64>, &str> = HashMap::new();
    // map.insert(OrderedFloat(3.14), "pi");

    // Tương tự, BTreeMap cũng không dùng được vì cần Ord (chứ không chỉ PartialOrd)
    // let mut sorted: BTreeMap<f64, &str> = BTreeMap::new();  // không compile
}

Đừng "hack" bằng cách derive Eq thủ công cho struct chứa f64 — bạn sẽ tạo ra UB logic ở runtime khi có NaN. Hai cách chính thống:

  1. Convert sang bit pattern qua to_bits() nếu bạn chấp nhận coi NaN bit-equal với chính bit pattern đó (nhưng có nhiều NaN khác nhau!).
  2. Dùng crate ordered-float cung cấp OrderedFloat<T>NotNan<T> — wrapper bảo đảm total ordering, từ chối NaN từ đầu.
7

Tránh So Sánh Equality Float

Như đã thấy ở mục 3, so sánh equality trực tiếp giữa hai float là rủi ro. Idiom Rust (cũng là idiom chung mọi ngôn ngữ): so sánh xấp xỉ qua một epsilon — sai số chấp nhận được.

fn main() {
    let a = 0.1_f64 + 0.2;
    let b = 0.3_f64;

    println!("{a} == {b} ? {}", a == b);  // false!
    println!("a = {a:.20}");              // 0.30000000000000004440
    println!("b = {b:.20}");              // 0.29999999999999998890

    // An toàn:
    const EPSILON: f64 = 1e-9;
    let almost_equal = (a - b).abs() < EPSILON;
    println!("almost equal? {almost_equal}");  // true
}

Lựa chọn EPSILON thực tế:

  • f64: 1e-9 đến 1e-12 cho giá trị "human-scale" (vài đơn vị đến vài triệu).
  • f32: 1e-6 đến 1e-7 — precision thấp hơn nên epsilon to hơn.
  • Stdlib có sẵn f64::EPSILON ≈ 2.22 × 10-16f32::EPSILON ≈ 1.19 × 10-7, nhưng đây là machine epsilon (sai số nhỏ nhất nhận biết được giữa 2 float liên tiếp gần 1.0) — thường quá nhỏ cho so sánh thực tế sau khi đã tính toán tích lũy lỗi.

Với số lớn hoặc rất nhỏ, dùng relative comparison thay vì absolute:

fn approx_eq(a: f64, b: f64, rel_tol: f64, abs_tol: f64) -> bool {
    // Kết hợp absolute và relative tolerance
    let diff = (a - b).abs();
    diff <= abs_tol || diff <= rel_tol * a.abs().max(b.abs())
}

fn main() {
    // Số lớn: 1_000_000.0 và 1_000_000.001 - chỉ khác 0.001
    // Absolute epsilon 1e-9 sẽ ra false (sai số quá lớn theo tuyệt đối)
    // Relative tolerance 1e-9 ra true (relative diff = 1e-9)
    println!("{}", approx_eq(1_000_000.0, 1_000_000.001, 1e-9, 1e-12)); // true

    // Số rất nhỏ: 1e-15 và 2e-15
    // Absolute 1e-9 ra true (sai khác quá nhỏ), nhưng giá trị thực sự khác nhau gấp đôi!
    // Relative 1e-9 ra false - đúng kỳ vọng
    println!("{}", approx_eq(1e-15, 2e-15, 1e-9, 1e-18)); // false
}

Trong code production, dùng crate approx cung cấp macro assert_relative_eq!, assert_abs_diff_eq! tiện hơn viết thủ công.

8

Method Phổ Biến

Cả f32f64 đều có cùng bộ method (chỉ khác type). Liệt kê các method dùng thường xuyên nhất:

fn main() {
    // Căn bậc 2 và luỹ thừa
    let s = 25.0_f64.sqrt();          // 5.0
    let cube_root = 27.0_f64.cbrt();  // 3.0
    let p1 = 2.0_f64.powi(10);        // 1024.0 - powi nhận i32 mũ (nhanh hơn)
    let p2 = 2.0_f64.powf(0.5);       // ~1.414 - powf nhận f64 mũ (linh hoạt)

    println!("sqrt(25)={s}, cbrt(27)={cube_root}, 2^10={p1}, 2^0.5={p2:.4}");

    // Lượng giác (radian, không phải degree)
    let pi = std::f64::consts::PI;
    let sin_val = (pi / 2.0).sin();   // 1.0
    let cos_val = pi.cos();           // -1.0
    let tan_val = (pi / 4.0).tan();   // 1.0
    let atan2 = 1.0_f64.atan2(1.0);   // pi/4 ~= 0.785

    println!("sin(pi/2)={sin_val}, cos(pi)={cos_val:.1}, tan(pi/4)={tan_val:.4}");

    // Logarithm và exponential
    let e = std::f64::consts::E;
    let ln_e = e.ln();                // 1.0 (natural log)
    let log10_1000 = 1000.0_f64.log10();   // 3.0
    let log2_8 = 8.0_f64.log2();           // 3.0
    let exp_1 = 1.0_f64.exp();             // e ~= 2.718

    println!("ln(e)={ln_e}, log10(1000)={log10_1000}, log2(8)={log2_8}");
    println!("exp(1)={exp_1:.4}");

    // Làm tròn - bốn flavor khác nhau
    let x = 3.7_f64;
    let y = -3.7_f64;
    println!("floor(3.7)={}, ceil(3.7)={}, round(3.7)={}, trunc(3.7)={}",
             x.floor(), x.ceil(), x.round(), x.trunc());
    // floor=3, ceil=4, round=4, trunc=3
    println!("floor(-3.7)={}, ceil(-3.7)={}, round(-3.7)={}, trunc(-3.7)={}",
             y.floor(), y.ceil(), y.round(), y.trunc());
    // floor=-4, ceil=-3, round=-4, trunc=-3

    // Phần fractional
    println!("fract(3.7)={}", x.fract());  // 0.7 (xấp xỉ)

    // abs, min, max
    let a = -5.5_f64;
    let b = 3.2_f64;
    println!("abs(-5.5)={}, min(-5.5, 3.2)={}, max(-5.5, 3.2)={}",
             a.abs(), a.min(b), a.max(b));

    // Tổng hợp: tính khoảng cách Euclidean 2D
    let (dx, dy) = (3.0_f64, 4.0_f64);
    let distance = (dx * dx + dy * dy).sqrt();
    println!("distance = {distance}");  // 5.0 (3-4-5 triangle)

    // Hyperbolic, gamma, erf cũng có sẵn: sinh, cosh, tanh, asinh, ...
}

Tip nhỏ: powi(n) cho mũ i32 nhanh hơn powf(n as f64) nhờ thuật toán exponentiation by squaring. Khi mũ là số nguyên nhỏ, luôn ưu tiên powi.

Constants hữu ích trong std::f64::consts (và std::f32::consts): PI, E, TAU (= 2π), SQRT_2, LN_2, LN_10, LOG2_E, LOG10_E.

9

Khi Nào Chọn f32 vs f64

Rule of thumb:

  • Mặc định dùng f64 — cho calculation thông thường, business logic, scientific computing, simulation. Đây là lựa chọn đúng 95% trường hợp; precision dư dả, performance trên CPU 64-bit ngang f32.
  • Dùng f32 khi giới hạn memory hoặc cần khớp định dạng external:
    • Graphics: vertex buffer GPU thường dùng fp32 (OpenGL, Vulkan, WebGL).
    • Machine learning inference: nhiều model deploy ở fp32 hoặc fp16; dùng f32 để khớp tensor dtype.
    • Embedded / IoT có RAM giới hạn — half memory footprint quan trọng khi xử lý array hàng triệu phần tử.
    • Data interchange với hệ thống cũ chỉ support 32-bit float (binary protocol, một số file format khoa học).
  • TUYỆT ĐỐI KHÔNG dùng float cho tiền tệ. Sai số rounding tích luỹ qua nhiều phép cộng/trừ sẽ tạo ra chênh lệch ở cent/đồng — nhỏ với một transaction, lớn dần khi audit hàng triệu giao dịch. Hai cách đúng:
    • Integer cents: lưu i64 số xu (vd $1.50 = 150). Phép cộng/trừ luôn chính xác; chia thì xử lý phần dư rõ ràng.
    • Decimal crate: rust_decimal (financial) hoặc bigdecimal (arbitrary precision) cung cấp type Decimal chính xác base-10, hỗ trợ scale tuỳ chỉnh.

Bảng quyết định nhanh:

Use case Lựa chọn
Tính toán khoa học, kỹ thuật, statistics f64
Vertex graphics, ML fp32 model, GPU compute f32
Khoảng cách GPS, geo coordinate f64 (precision lat/long quan trọng)
Game physics (position, velocity) f32 thường đủ (engine như Bevy default f32)
Currency, accounting, tax calc Không dùng float — integer cents hoặc rust_decimal
Probability, ratio (0..1) f64
Sensor reading (IoT, embedded) f32 (đủ precision, tiết kiệm RAM)
10

Tổng Kết

  • Rust có 2 floating-point: f32 (single, 32-bit, ~7 digit) và f64 (double, 64-bit, ~15-17 digit). f64 là default — literal 1.0 ngầm định là f64.
  • Cả hai theo chuẩn IEEE-754 binary: sign + exponent + mantissa. Hệ quả: float chỉ chính xác với các phân số m / 2^n; vì vậy 0.1 + 0.2 != 0.3.
  • Giá trị đặc biệt: NaN, INFINITY, NEG_INFINITY, negative zero. Check qua is_nan(), is_infinite(), is_finite(). NaN "lây" — mọi phép toán có NaN đều trả NaN.
  • Float chỉ implement PartialEq (không Eq) vì NaN == NaNfalse — vi phạm reflexive. Không dùng f64 trực tiếp làm key cho HashMap; workaround qua to_bits() hoặc crate ordered-float.
  • Tránh so sánh equality trực tiếp; dùng (a - b).abs() < EPSILON. Epsilon thực tế: 1e-9 cho f64, 1e-6 cho f32. Với số rất lớn / rất nhỏ dùng relative tolerance.
  • Method phổ biến: sqrt, powi/powf, sin/cos/tan, ln/log10/log2, floor/ceil/round/trunc, abs, min/max. Constants trong std::f64::consts.
  • Default f64 cho mọi calculation; chọn f32 khi giới hạn memory hoặc cần khớp GPU/ML. TUYỆT ĐỐI KHÔNG dùng float cho tiền tệ — dùng integer cents hoặc rust_decimal.
11

Bài Tập Củng Cố

Tự trả lời, đáp án ở cuối:

  1. Tại sao 0.1 + 0.2 == 0.3 trả về false trong Rust (và mọi ngôn ngữ IEEE-754 khác)? Giải thích ngắn dựa trên cấu trúc mantissa của f64.
  2. Đoạn code sau không compile: let mut m: HashMap<f64, String> = HashMap::new(); m.insert(3.14, "pi".into());. Lỗi cụ thể là gì? Đề xuất 2 cách workaround.
  3. Bạn cần so sánh hai kết quả tính toán xy trên f64 với sai số chấp nhận được 1e-9. Viết một dòng code đúng idiom Rust để check "x xấp xỉ y".
  4. Một fintech junior viết hàm tính lãi suất kép với f64 rồi nhân lên qua hàng triệu giao dịch — audit phát hiện tổng lệch vài đồng so với DB. Vấn đề ở đâu? Hai cách fix?
  5. Phép tính (-1.0_f64).sqrt() trả về giá trị gì? Phép 1.0_f64 / 0.00.0_f64 / 0.0 trả về giá trị gì? Cách check 3 trường hợp này trong code?
Đáp án
  1. Mantissa 52-bit của f64 không thể biểu diễn chính xác 0.1 hay 0.2 — trong binary chúng là phân số tuần hoàn vô hạn (giống 1/3 = 0.333... trong decimal). Hệ thống làm tròn (round-to-nearest) khi lưu vào 52 bit. Tổng hai số xấp xỉ → kết quả ~ 0.30000000000000004, không trùng với xấp xỉ của 0.3 (~ 0.29999999999999998). Đây là đặc tính của IEEE-754, không phải bug của Rust.
  2. Lỗi: the trait bound `f64: Eq` is not satisfied (và kéo theo f64: Hash cũng không thoả). HashMap yêu cầu key Eq + Hash; f64 chỉ có PartialEqNaN != NaN. Workaround: (a) lưu key qua f64::to_bits() sang u64 rồi dùng HashMap<u64, V>; (b) dùng crate ordered-float với HashMap<OrderedFloat<f64>, V> hoặc NotNan<f64> (từ chối NaN ngay từ đầu).
  3. let close = (x - y).abs() < 1e-9; — tính sai số tuyệt đối và so với epsilon. Production code có thể tách const EPSILON: f64 = 1e-9; ở module level, hoặc dùng macro approx::assert_relative_eq! nếu giá trị có range rộng.
  4. Float tích luỹ rounding error qua mỗi phép cộng/nhân. Sau hàng triệu giao dịch, sai số nhỏ thành đáng kể (vài đồng, thậm chí vài trăm). Hai cách fix chuẩn: (a) lưu tiền dưới dạng integer cents (i64) — mọi cộng/trừ chính xác tuyệt đối, chia thì xử lý phần dư minh bạch; (b) dùng crate rust_decimal với type Decimal chính xác base-10, hỗ trợ scale tuỳ chỉnh, là chuẩn industry cho financial calculation trong Rust.
  5. (-1.0).sqrt() trả NaN (căn của số âm vô nghĩa trong tập real). 1.0 / 0.0 trả f64::INFINITY. 0.0 / 0.0 trả NaN (vô định). Cách check: x.is_nan() cho NaN, x.is_infinite() cho ±Infinity; thường dùng combo x.is_finite() để loại cả ba (trả true chỉ khi số bình thường — không NaN, không Inf, không NegInf).
12

Bài Tiếp Theo

Bài 35: Boolean: bool, true / false — sang scalar type tiếp theo trong nhóm: bool chỉ có 2 giá trị true/false nhưng chiếm 1 byte trong memory, operator ! / && / || short-circuit, vì sao Rust không tự convert bool sang integer (khác C), và cách cast tường minh qua as u8 khi cần.