Mục lục
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
usizelàm index choVec, array, slice — không phảiu32hayi32. - Biết
u8là kiểu chuẩn cho byte:Vec<u8>làm buffer,&[u8]làm slice byte, byte literalb'A'. - Biết khi nào chọn
u32(counter ngắn, session id), khi nào chọnu64(timestamp ms, large counter, hash 64-bit), khi nào cầnu128(UUID raw, crypto, big number). - Hiểu pitfall underflow đặc trưng của unsigned:
5_u32 - 10_u32panic ở debug, wrap-around ở release. Cách fix bằngchecked_sub,saturating_sub, hoặc cast sangi64. - Biết hai cách convert signed ↔ unsigned:
ascast bitwise (nhanh nhưng có thể lossy/đổi nghĩa) vàtry_fromtrả vềResultan 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.
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;.
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ùngu32.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.
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
Veckhô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ả
u32vàu64là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.
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:
StringRust đảm bảo valid UTF-8 — không phảiVec<u8>bất kỳ. ConvertVec<u8> → Stringphải quaString::from_utf8(v)trả vềResult.- Network protocol: tất cả byte trên wire là
u8. Library nhưtokio,bytes,tonicđều dùngVec<u8>/Bytes(wrapu8). - File I/O:
std::fs::read("a.bin")trả vềVec<u8>;read_to_stringtrả vềStringsau khi validate UTF-8. - Tránh nhầm
u8vớichar:charlà Unicode scalar 4-byte,u8là 1 byte. ASCII subset trùng nhau (0..127), nhưng emoji, ký tự tiếng Việt có dấu phải dùngchar.
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::Durationinternal (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, u32 và u64 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ô.
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]khii = 0→ underflow ngay. Fix: kiểm traif i > 0trước, hoặc dùngi.checked_sub(1). - Tính duration:
end - startgiữ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 sangi64.
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.
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):
parse::<i32>()hoặcparse::<i64>()để bắt error format trước.u32::try_from(parsed)để convert sang unsigned và bắt error âm/vượt range.- Map error về domain error type của app (hoặc dùng
?+Fromimpl).
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).
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.
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. usizebắt buộc cho indexVec/ array / slice; size = pointer size (32 hoặc 64 bit tùy target).u8là byte chuẩn:Vec<u8>buffer,&[u8]slice, literalb'X',b"...".- Chọn type theo upper bound:
u32cho counter/id thường,u64cho timestamp ms & hash,u128cho UUID/crypto. - Pitfall: subtract trên unsigned underflow → debug panic, release wrap. Fix:
checked_sub,saturating_sub, hoặc cast sangi64. ascast bitwise — nhanh nhưng silently corrupts khi out-of-range.try_fromtrả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.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- 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;? - Bạn đang viết hàm tính pagination: nhận
totalvàper_pagekiểuu32, 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. - Đoạn code
let len: u32 = my_vec.len();không compile. Vì sao và sửa thế nào? - Trong handler HTTP nhận
user_idtừ query string. Bạn parse thànhi32rồi cast sangu32bằngparsed as u32. Đây là bug. Mô tả bug đó và viết lại bằng cách an toàn. - Trên target wasm32,
usize::MAXlà bao nhiêu? Vì sao Vec<u8>trên wasm32 không thể chứa quá ~4 GB?
Đáp án
let b: u8 = 256;không hợp lệ — vượtu8::MAX = 255, compiler errorliteral out of range.let d: u32 = -1;không hợp lệ — unsigned không chứa giá trị âm, errorunary operator - cannot be applied to type u32(hoặccannot apply unary operator). Hai dòngavàchợp lệ (đúng MAX).let pages: u32 = total.div_ceil(per_page);. Methoddiv_ceilcó 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.- Vì
Vec::len()trả vềusize, không phảiu32. Trên target 64-bit,usize=u64, không tự convert sangu32. Sửa:let len: usize = my_vec.len();(giữ usize) hoặclet len: u32 = my_vec.len() as u32;(cast, có thể truncate nếu len > u32::MAX) hoặclet len = u32::try_from(my_vec.len()).expect("...");(an toàn). - Bug: nếu user gửi
user_id=-1, parse thànhi32 = -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_intotrả error nếu âm. - Trên wasm32,
usize::MAX = u32::MAX = 4_294_967_295(~4 GB). VìVec<u8>có len kiểuusize, không thể quáusize::MAX; mà mỗiu8= 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.
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.
