Danh sách bài viết

Bài 32: Integer Không Dấu: u8, u16, u32, u64, u128, usize

Bài 32 của series Rust Cơ Bản — 6 loại unsigned integer: range mỗi type, vì sao usize bắt buộc cho index Vec / array, u8 là kiểu chuẩn cho byte buffer, khi nào chọn u32 vs u64, pitfall underflow đặc trưng của unsigned, conversion an toàn signed ↔ unsigned, và method utility phổ biến.

09/06/2026
10 phút đọc
2 lượt xem
1

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

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

  • Nắm rõ 6 size integer không dấu của Rust: u8, u16, u32, u64, u128, usize; range của từng loại và use case điển hình.
  • Biết vì sao Rust bắt buộc dùng usize làm index cho Vec, array, slice — không phải u32 hay i32.
  • Biết u8 là kiểu chuẩn cho byte: Vec<u8> làm buffer, &[u8] làm slice byte, byte literal b'A'.
  • Biết khi nào chọn u32 (counter ngắn, session id), khi nào chọn u64 (timestamp ms, large counter, hash 64-bit), khi nào cần u128 (UUID raw, crypto, big number).
  • Hiểu pitfall underflow đặc trưng của unsigned: 5_u32 - 10_u32 panic ở debug, wrap-around ở release. Cách fix bằng checked_sub, saturating_sub, hoặc cast sang i64.
  • Biết hai cách convert signed ↔ unsigned: as cast bitwise (nhanh nhưng có thể lossy/đổi nghĩa) và try_from trả về Result an toàn cho input từ user.
  • Nắm các method utility hay dùng: pow, leading_zeros, count_ones, ilog2, div_ceil, next_power_of_two.
2

Tổng Quan 6 Loại Unsigned

Rust cung cấp 6 size unsigned integer, song song với 6 size signed đã học ở bài 31. Khác biệt cốt lõi: unsigned chỉ chứa giá trị không âm, range trải từ 0 đến 2^N - 1 (thay vì -2^(N-1)..2^(N-1)-1 như signed).

Type Bit Range Use case
u8 8 0 .. 255 byte, raw bytes file/network
u16 16 0 .. 65,535 port number, small id
u32 32 0 .. ~4.3 tỷ session id, request count
u64 64 0 .. ~1.8 × 1019 timestamp ms, large counter, hash
u128 128 0 .. ~3.4 × 1038 UUID raw, crypto
usize pointer-size 0 .. (232 hoặc 264) − 1 Vec/array index

So với signed: cùng số bit, unsigned gấp đôi giá trị dương tối đa nhưng mất hết giá trị âm. Ví dụ i8 chứa -128..127 (256 giá trị, 128 dương), u8 chứa 0..255 (256 giá trị, 255 dương). Khi biết chắc giá trị không bao giờ âm (count, length, id), unsigned cho range dương rộng hơn và bắt được lỗi sớm (gán âm vào unsigned không compile).

Lưu ý: default integer của Rust là i32, không phải u32. Khi viết let x = 5; không annotate, compiler chọn i32. Muốn unsigned phải annotate rõ: let x: u32 = 5; hoặc dùng suffix let x = 5u32;.

3

Range Mỗi Type

Mỗi unsigned type có MIN luôn là 0, MAX là 2^N - 1. Rust expose hằng số ::MIN, ::MAX trên từng type:

fn main() {
    println!("u8   : {} .. {}", u8::MIN, u8::MAX);       // 0 .. 255
    println!("u16  : {} .. {}", u16::MIN, u16::MAX);     // 0 .. 65535
    println!("u32  : {} .. {}", u32::MIN, u32::MAX);     // 0 .. 4294967295
    println!("u64  : {} .. {}", u64::MIN, u64::MAX);     // 0 .. 18446744073709551615
    println!("u128 : {} .. {}", u128::MIN, u128::MAX);   // 0 .. 340282366920938463463374607431768211455
    println!("usize: {} .. {}", usize::MIN, usize::MAX); // phụ thuộc target

    // usize cụ thể trên target 64-bit:
    // 0 .. 18446744073709551615  (= u64::MAX)

    // Trên target 32-bit (vd Cortex-M, wasm32):
    // 0 .. 4294967295  (= u32::MAX)
}

Một số mốc dễ nhớ trong production:

  • u8::MAX = 255 — một byte chứa được giá trị 0..255 không dấu. ASCII printable chỉ tới 127.
  • u16::MAX = 65,535 — đúng range port TCP/UDP (0..65535), nên port number trong stdlib (std::net::SocketAddr) là u16.
  • u32::MAX ≈ 4.29 × 109 — đủ cho hầu hết counter / id trong app trung bình. Bitcoin block height vẫn dùng u32.
  • u64::MAX ≈ 1.8 × 1019 — đủ cho timestamp nano từ Unix epoch trong vài trăm năm; đủ cho 64-bit hash (FNV, xxHash).
  • u128::MAX ≈ 3.4 × 1038 — đủ cho UUID v4 (đúng 128 bit), big number cho RSA/elliptic curve small.

Khi value vượt MAX khi assign trực tiếp, compiler báo error ngay: let x: u8 = 256;error: literal out of range for u8. Khi vượt qua phép tính runtime (vd cộng dồn), hành vi là overflow — sẽ học sâu ở bài 33.

4

usize — Type Cho Index Collection

usize là type bắt buộc để index Vec, array, slice trong Rust. Nó được định nghĩa là "unsigned integer có cùng số bit với pointer trên target hiện tại": 32 bit trên target 32-bit (wasm32, ARM Cortex), 64 bit trên target 64-bit (x86_64, aarch64).

fn main() {
    let v: Vec<&str> = vec!["alpha", "beta", "gamma", "delta"];

    // Index PHẢI là usize - không phải u32/i32
    let i: usize = 2;
    println!("{}", v[i]); // "gamma"

    // Literal số nguyên trong index được infer thành usize
    println!("{}", v[0]); // OK - 0 auto thành usize
    println!("{}", v[1]); // OK

    // Truyền u32 làm index - KHÔNG compile
    // let j: u32 = 1;
    // println!("{}", v[j]); // error: expected usize, found u32
    // Fix: ép kiểu
    let j: u32 = 1;
    println!("{}", v[j as usize]); // "beta"

    // .len() trả về usize - khớp với index type
    let len: usize = v.len();
    println!("len = {}", len);

    // Iterate qua range usize
    for i in 0..v.len() {
        println!("v[{i}] = {}", v[i]);
    }
}

Vì sao bắt buộc usize? Hai lý do:

  • Address space match pointer size: số phần tử tối đa của một Vec không vượt quá số byte address-able trong RAM. Trên máy 64-bit, address space 264; trên máy 32-bit, address space 232. usize đúng kích thước này — đảm bảo index không bao giờ "không đủ chỗ" hoặc "dư thừa".
  • Tránh ambiguity: nếu cho phép cả u32u64 làm index, compiler phải insert cast ngầm — sinh confusion và bug khi cross-platform. Rust ép một loại duy nhất.

Method liên quan đều trả về usize: Vec::len(), str::len(), slice::iter().count(), HashMap::len(). Khi tính toán liên quan đến size collection (capacity, offset, byte length), luôn dùng usize.

5

u8 Cho Byte

u8 là kiểu chuẩn cho byte trong Rust. Mọi API I/O cấp thấp (file, network socket, serialization) đều xài u8: Vec<u8> làm buffer khả mở rộng, &[u8] làm slice đọc-only, &mut [u8] làm slice ghi.

use std::io::Read;

fn main() {
    // Vec<u8> làm buffer
    let mut buffer: Vec<u8> = Vec::with_capacity(1024);
    buffer.push(72);   // 'H'
    buffer.push(105);  // 'i'
    buffer.push(33);   // '!'
    println!("{:?}", buffer); // [72, 105, 33]

    // String → &[u8] (bytes của UTF-8)
    let s = "Hello";
    let bytes: &[u8] = s.as_bytes();
    println!("{:?}", bytes); // [72, 101, 108, 108, 111]

    // Byte literal: b'X' = u8 value của ký tự ASCII
    let a: u8 = b'A';        // 65
    let zero: u8 = b'0';     // 48
    let newline: u8 = b'\n'; // 10
    println!("A = {}, 0 = {}, \\n = {}", a, zero, newline);

    // Byte string literal: b"..." = &[u8; N]
    let magic: &[u8; 4] = b"\x89PNG";
    println!("{:?}", magic); // [137, 80, 78, 71]

    // char → u8: chỉ an toàn cho ASCII
    let c: char = 'Z';
    let cu: u8 = c as u8;    // 90 - OK vì 'Z' < 128
    println!("Z = {}", cu);

    // CẢNH BÁO: char Unicode không ASCII → as u8 truncate, mất data
    let emoji: char = '🦀';
    let bad: u8 = emoji as u8;  // truncate 4-byte codepoint còn 1 byte cuối
    println!("🦀 as u8 = {} (KHÔNG có nghĩa)", bad);

    // Đúng: dùng try_into để bắt lỗi
    let safe: Result<u8, _> = u8::try_from(emoji as u32);
    println!("try_from: {:?}", safe); // Err(...)
}

Một số quy tắc quan trọng khi làm việc với u8:

  • String Rust đảm bảo valid UTF-8 — không phải Vec<u8> bất kỳ. Convert Vec<u8> → String phải qua String::from_utf8(v) trả về Result.
  • Network protocol: tất cả byte trên wire là u8. Library như tokio, bytes, tonic đều dùng Vec<u8> / Bytes (wrap u8).
  • File I/O: std::fs::read("a.bin") trả về Vec<u8>; read_to_string trả về String sau khi validate UTF-8.
  • Tránh nhầm u8 với char: char là Unicode scalar 4-byte, u8 là 1 byte. ASCII subset trùng nhau (0..127), nhưng emoji, ký tự tiếng Việt có dấu phải dùng char.
6

Khi Nào Dùng u32 vs u64

Quyết định u32 hay u64 dựa trên upper bound ước tính và đặc thù use case. Rule of thumb:

  • u32 (~4.3 tỷ): session id sống ngắn, request count trong 1 process, port-range internal, version number, color RGBA packed, ID auto-increment cho table nhỏ-vừa.
  • u64 (~1.8 × 1019): timestamp millisecond hoặc nanosecond từ Unix epoch, large counter accumulator (analytics global), hash 64-bit (FNV1a, xxHash3-64, SipHash), file size (file lớn vượt 4 GB), database row id 64-bit (Snowflake, Twitter ID).
  • u128 (~3.4 × 1038): UUID v4 raw (đúng 128 bit), big number crypto (modular arithmetic small), std::time::Duration internal (giây + nano), TimeStamp UUID v7.
use std::time::{SystemTime, UNIX_EPOCH};

fn main() {
    // u32 - session id, request count
    let session_id: u32 = 1_234_567;
    let request_count: u32 = 50_000;
    println!("session = {session_id}, req = {request_count}");

    // u64 - timestamp ms từ epoch
    let now_ms: u64 = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_millis() as u64;
    println!("now = {now_ms} ms");

    // u64 - hash
    let hash: u64 = 0xDEADBEEFCAFEBABE;
    println!("hash = {hash:#018x}");

    // u128 - UUID v4 raw (128 bit)
    let uuid_raw: u128 = 0x550e8400_e29b_41d4_a716_446655440000;
    println!("uuid = {uuid_raw:#034x}");
}

Lưu ý ngầm về performance: trên CPU 64-bit hiện đại, u32u64 có chi phí số học gần như nhau. Khác biệt chỉ rõ rệt khi (a) lưu trong memory dạng array dài (u32 tốn nửa RAM của u64), (b) truyền qua mạng (serialize ngắn hơn), (c) trên 32-bit target (u64 cần 2 register). Trong app web/backend phổ thông, ưu tiên chọn type vừa đủ semantic hơn là tối ưu vi mô.

7

Pitfall: Underflow Trên Unsigned

Đây là pitfall đặc trưng nhất của unsigned và là nguồn bug phổ biến của người mới Rust. Vì unsigned không chứa giá trị âm, phép trừ ra kết quả nhỏ hơn 0 sẽ underflow:

fn main() {
    let a: u32 = 5;
    let b: u32 = 10;

    // let c = a - b;
    // Debug build: panic 'attempt to subtract with overflow'
    // Release build: wrap-around về 4294967291 (= u32::MAX - 4)

    // Safe alternative 1: checked_sub - trả Option
    let c1: Option<u32> = a.checked_sub(b);
    println!("checked_sub: {:?}", c1); // None

    // Safe alternative 2: saturating_sub - clamp về MIN (= 0)
    let c2: u32 = a.saturating_sub(b);
    println!("saturating_sub: {}", c2); // 0

    // Safe alternative 3: cast lên signed lớn hơn trước
    let c3: i64 = (a as i64) - (b as i64);
    println!("cast i64: {}", c3); // -5

    // Trường hợp thực tế: tính duration giữa 2 timestamp
    let t_start: u64 = 1_000_000;
    let t_end: u64 = 1_500_000;
    let elapsed = t_end.saturating_sub(t_start); // 500_000 - an toàn ngay cả khi clock skew

    println!("elapsed = {elapsed} ms");
}

Hai bối cảnh underflow xuất hiện nhiều nhất trong code thật:

  • Subtract index: v[i - 1] khi i = 0 → underflow ngay. Fix: kiểm tra if i > 0 trước, hoặc dùng i.checked_sub(1).
  • Tính duration: end - start giữa hai timestamp. Nếu clock skew hoặc dữ liệu sai thứ tự, start > end → underflow. Fix: end.saturating_sub(start) trả 0 khi đảo ngược, hoặc cast cả hai sang i64.

Rust cố tình KHÔNG tự động "wrap silently" ở debug build — panic giúp bạn phát hiện bug ngay khi chạy test. Ở release build, vì lý do performance, overflow wrap. Bài 33 sẽ đi sâu vào 4 family method (checked_*, wrapping_*, saturating_*, overflowing_*) cho mọi phép số học.

8

Conversion Signed ↔ Unsigned

Có hai cách convert giữa signed và unsigned, ý nghĩa rất khác nhau.

Cách 1: as cast — bitwise, nhanh, không an toàn

fn main() {
    // i32 → u32: cast bitwise (two's complement)
    let neg: i32 = -1;
    let pos: u32 = neg as u32;
    println!("{neg} → {pos}");        // -1 → 4294967295 (0xFFFFFFFF)

    let neg2: i32 = -100;
    let pos2: u32 = neg2 as u32;
    println!("{neg2} → {pos2}");      // -100 → 4294967196

    // u32 → i32: cũng bitwise
    let big: u32 = u32::MAX;          // 4294967295
    let signed: i32 = big as i32;
    println!("{big} → {signed}");     // 4294967295 → -1

    // Truncation khi cast xuống nhỏ hơn
    let x: u32 = 300;
    let y: u8 = x as u8;
    println!("{x} as u8 = {y}");      // 300 as u8 = 44 (= 300 - 256)
}

as KHÔNG check value, luôn thực hiện reinterpret/truncate. Phù hợp khi (a) bạn chắc chắn value nằm trong range đích, (b) bạn cố ý dùng bit pattern (vd FFI với C, encode/decode binary protocol). KHÔNG phù hợp khi input đến từ user / network / file — vì lúc đó -1 → 4294967295 là bug, không phải feature.

Cách 2: try_from / try_into — an toàn, trả Result

fn parse_age(input: &str) -> Result<u32, String> {
    let parsed: i32 = input.parse().map_err(|e: std::num::ParseIntError| e.to_string())?;
    // Convert i32 → u32 an toàn: fail nếu âm
    u32::try_from(parsed).map_err(|_| format!("age must be non-negative, got {parsed}"))
}

fn main() {
    println!("{:?}", parse_age("25"));   // Ok(25)
    println!("{:?}", parse_age("-1"));   // Err("age must be non-negative, got -1")
    println!("{:?}", parse_age("abc"));  // Err("invalid digit found in string")

    // try_from u64 → u32: fail nếu vượt range
    let big: u64 = 5_000_000_000;
    let r: Result<u32, _> = u32::try_from(big);
    println!("{:?}", r); // Err(TryFromIntError(()))

    let small: u64 = 100;
    let r2: Result<u32, _> = u32::try_from(small);
    println!("{:?}", r2); // Ok(100)
}

Idiom Rust khi parse input từ user (CLI args, HTTP request body, JSON field):

  1. parse::<i32>() hoặc parse::<i64>() để bắt error format trước.
  2. u32::try_from(parsed) để convert sang unsigned và bắt error âm/vượt range.
  3. Map error về domain error type của app (hoặc dùng ? + From impl).

Tránh dùng as với input không tin cậy — nó silently corrupts data. try_from dài hơn một dòng nhưng tránh được class bug nghiêm trọng (auth bypass khi user_id = -1 cast thành u32 max).

9

Method Phổ Biến

Mỗi unsigned type có hàng chục method utility. Dưới đây là nhóm hay gặp nhất trong code production:

fn main() {
    // pow - lũy thừa nguyên
    let kb: u32 = 1024;
    let mb: u32 = kb.pow(2);              // 1024^2 = 1_048_576
    println!("1 MB = {mb} bytes");

    // leading_zeros / trailing_zeros - đếm bit 0 đầu/cuối
    let x: u32 = 0b0000_0000_0000_0000_0000_0001_0000_0000; // = 256
    println!("leading_zeros: {}", x.leading_zeros());   // 23
    println!("trailing_zeros: {}", x.trailing_zeros()); // 8

    // count_ones / count_zeros - đếm số bit 1 / 0 (population count)
    let y: u32 = 0b1011_0110;             // 4 bit 1
    println!("count_ones: {}", y.count_ones());  // 4
    println!("count_zeros: {}", y.count_zeros()); // 28 (32 - 4)

    // ilog2 - integer log base 2 (làm tròn xuống). Panic nếu x = 0.
    let n: u32 = 1000;
    println!("ilog2({n}) = {}", n.ilog2()); // 9 (vì 2^9 = 512 ≤ 1000 < 1024 = 2^10)

    // div_ceil - chia làm tròn LÊN. Tránh phải tự viết (a + b - 1) / b.
    let total_items: u32 = 100;
    let per_page: u32 = 30;
    let pages: u32 = total_items.div_ceil(per_page); // 4 (100/30 = 3.33 → 4)
    println!("pages = {pages}");

    // next_power_of_two - tìm lũy thừa của 2 nhỏ nhất ≥ x. Hữu ích khi cấp buffer.
    let need: u32 = 1000;
    let cap: u32 = need.next_power_of_two(); // 1024
    println!("cap = {cap}");

    // is_power_of_two
    println!("1024 is pow2: {}", 1024_u32.is_power_of_two()); // true
    println!("1000 is pow2: {}", 1000_u32.is_power_of_two()); // false

    // to_be_bytes / to_le_bytes / from_be_bytes / from_le_bytes - serialize
    let v: u32 = 0x12345678;
    let be: [u8; 4] = v.to_be_bytes();    // [0x12, 0x34, 0x56, 0x78]
    let le: [u8; 4] = v.to_le_bytes();    // [0x78, 0x56, 0x34, 0x12]
    println!("BE = {be:02x?}");
    println!("LE = {le:02x?}");
}

Nhóm method này có sẵn cho tất cả size unsigned (u8 đến u128, usize). Khi xử lý bit-twiddling, packed data, encoding, hay cần tránh viết tay logic dễ sai, đọc qua trang doc std::primitive::u32 để biết có method gì sẵn.

10

Tổng Kết

  • Rust có 6 size unsigned: u8, u16, u32, u64, u128, usize — range 0 .. 2N − 1.
  • Default integer là i32 — muốn unsigned phải annotate hoặc dùng suffix.
  • usize bắt buộc cho index Vec / array / slice; size = pointer size (32 hoặc 64 bit tùy target).
  • u8 là byte chuẩn: Vec<u8> buffer, &[u8] slice, literal b'X', b"...".
  • Chọn type theo upper bound: u32 cho counter/id thường, u64 cho timestamp ms & hash, u128 cho UUID/crypto.
  • Pitfall: subtract trên unsigned underflow → debug panic, release wrap. Fix: checked_sub, saturating_sub, hoặc cast sang i64.
  • as cast bitwise — nhanh nhưng silently corrupts khi out-of-range. try_from trả Result — dùng cho input không tin cậy.
  • Method utility: pow, leading_zeros, count_ones, ilog2, div_ceil, next_power_of_two, to_be_bytes.
11

Bài Tập Củng Cố

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

  1. Trong các literal sau, literal nào KHÔNG hợp lệ cho type chỉ định: let a: u8 = 255;, let b: u8 = 256;, let c: u16 = 65_535;, let d: u32 = -1;?
  2. Bạn đang viết hàm tính pagination: nhận totalper_page kiểu u32, trả về số page. Viết 1 dòng dùng method stdlib, không viết tay công thức (total + per_page - 1) / per_page.
  3. Đoạn code let len: u32 = my_vec.len(); không compile. Vì sao và sửa thế nào?
  4. Trong handler HTTP nhận user_id từ query string. Bạn parse thành i32 rồi cast sang u32 bằng parsed as u32. Đây là bug. Mô tả bug đó và viết lại bằng cách an toàn.
  5. Trên target wasm32, usize::MAX là bao nhiêu? Vì sao Vec<u8> trên wasm32 không thể chứa quá ~4 GB?
Đáp án
  1. let b: u8 = 256; không hợp lệ — vượt u8::MAX = 255, compiler error literal out of range. let d: u32 = -1; không hợp lệ — unsigned không chứa giá trị âm, error unary operator - cannot be applied to type u32 (hoặc cannot apply unary operator). Hai dòng ac hợp lệ (đúng MAX).
  2. let pages: u32 = total.div_ceil(per_page);. Method div_ceil có sẵn cho mọi integer type từ Rust 1.73, tránh được lỗi off-by-one khi viết tay công thức ceiling division.
  3. Vec::len() trả về usize, không phải u32. Trên target 64-bit, usize = u64, không tự convert sang u32. Sửa: let len: usize = my_vec.len(); (giữ usize) hoặc let len: u32 = my_vec.len() as u32; (cast, có thể truncate nếu len > u32::MAX) hoặc let len = u32::try_from(my_vec.len()).expect("..."); (an toàn).
  4. Bug: nếu user gửi user_id=-1, parse thành i32 = -1, cast -1 as u32 = 4_294_967_295 (u32::MAX). Server tưởng đây là user_id hợp lệ và có thể auth bypass / lookup record sai. Sửa: let id: u32 = query_param.parse::<i32>()?.try_into().map_err(|_| BadRequest)?;try_into trả error nếu âm.
  5. Trên wasm32, usize::MAX = u32::MAX = 4_294_967_295 (~4 GB). Vì Vec<u8> có len kiểu usize, không thể quá usize::MAX; mà mỗi u8 = 1 byte, nên dung lượng Vec tối đa ~4 GB. Đây là giới hạn cứng của address space 32-bit, không phải giới hạn của Rust.
12

Bài Tiếp Theo

Bài 33: Integer Overflow — Debug Panic vs Release Wrap — đào sâu hành vi overflow của Rust: vì sao debug panic, release wrap silently; cơ chế hai chế độ này; và 4 family method cho phép xử lý overflow chủ động: checked_* (Option), wrapping_* (luôn wrap), saturating_* (clamp về MIN/MAX), overflowing_* (tuple). Đây là kiến thức bắt buộc cho mọi code Rust production có số học.