Mục lục
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Viết được khai báo
const MAX_SIZE: usize = 100;đúng convention UPPER_SNAKE_CASE và biết vì sao type annotation là bắt buộc — không có infer. - Hiểu khái niệm const expression — vì sao
const X: Vec<i32> = vec![];không compile được trong khiconst X: i32 = 1 + 2;thì OK. - Phân biệt rõ
constvớiletqua 8 tiêu chí: scope, naming, type annotation, expression, mutability, memory layout, visibility, runtime semantics. - Hiểu vì sao
consttrong Rust khác hoàn toànconstcủa JavaScript dù cùng tên. - Biết Rust inline value của
consttại mỗi use-site → không có địa chỉ memory ổn định → preview lý do tồn tạistaticở bài 29. - Biết khi nào nên dùng
const(magic number, threshold, default config) và khi nào KHÔNG (String heap, Vec, HashMap dynamic).
Bài 27: Shadowing đã làm rõ let có thể "re-bind" cùng tên — nhưng vẫn là runtime binding. Bài 28 chuyển sang một khái niệm hoàn toàn khác: const sống ở compile time, không phải binding mà là tên đại diện cho một value cố định trong source code.
Cú Pháp const
Cú pháp tổng quát: const <NAME>: <Type> = <const_expression>;. Ba điểm BẮT BUỘC khác hẳn let:
// Khai báo const cơ bản
const MAX_SIZE: usize = 100;
const PI: f64 = 3.14159265358979;
const APP_NAME: &str = "blogcode";
const ENABLED: bool = true;
fn main() {
println!("MAX_SIZE = {MAX_SIZE}");
println!("PI = {PI}");
println!("APP_NAME = {APP_NAME}");
}
- Naming UPPER_SNAKE_CASE: tên const viết hoa toàn bộ, ngăn cách bằng dấu gạch dưới. Đây không chỉ là convention — clippy có lint
upper_case_acronymsvà compiler emit warningnon_upper_case_globalsnếu vi phạm. Mục đích: ở bất kỳ chỗ nào trong code thấyMAX_SIZE, người đọc lập tức biết đây là hằng số, không phải biến. - Type annotation bắt buộc: viết
const X = 100;sẽ không compile — lỗimissing type for `const` item. Rust quyết định không infer type cho const vì const là một item ở module level, không phải statement trong function body — type phải tường minh để consumer của module (và compiler khi đọc cross-module) đọc thẳng vào. - Expression phải là const expression: bên phải dấu
=không được phép là biểu thức tuỳ ý — phải là biểu thức mà compiler đánh giá được tại compile time (literal, phép toán trên literal/const khác, gọiconst fn...). Chi tiết ở mục 3.
Một khác biệt nhỏ về syntax: trong println!("{X}"), capture inline của Rust 2021+ vẫn hoạt động với const như với biến local — không cần thay đổi gì.
Const Expression — Hạn Chế
Compiler phải tính giá trị const ngay khi compile, vậy nên bên phải dấu = chỉ được dùng những thứ "const-evaluable":
- Literal:
100,3.14,"hello",true,'A'. - Phép arithmetic trên const/literal:
100 * 1024,MAX_SIZE + 1. - Gọi
const fn: hàm được đánh dấuconst fntức là compiler biết cách evaluate nó tại compile time. Std lib có nhiều const fn:i32::from_le_bytes,u32::pow,str::len,Option::is_some... - Reference tới const khác: chuỗi compose nhiều const.
- Cấu trúc đơn giản: tuple, array, struct với field-init bằng const expression.
Ví dụ const expression hợp lệ:
// Arithmetic compile-time
const SECONDS_PER_HOUR: u32 = 60 * 60;
const KB: usize = 1024;
const MB: usize = KB * 1024;
const GB: usize = MB * 1024;
// Gọi const fn của std
const NAME_LEN: usize = "blogcode".len(); // str::len là const fn
// Const struct
struct Color { r: u8, g: u8, b: u8 }
const BRAND_COLOR: Color = Color { r: 0x1E, g: 0x88, b: 0xE5 };
// Const array
const FIBONACCI: [u32; 6] = [1, 1, 2, 3, 5, 8];
Những thứ KHÔNG được phép trong const expression — sẽ compile error:
use std::collections::HashMap;
// ERROR: cannot call non-const fn `HashMap::<K, V>::new`
const CACHE: HashMap<String, i32> = HashMap::new();
// ERROR: cannot call non-const fn `Vec::<T>::new`
const ITEMS: Vec<i32> = Vec::new();
// ERROR: cannot call non-const fn `String::from`
const NAME: String = String::from("blogcode");
// ERROR: `vec!` expands to non-const Vec::new + push
const NUMS: Vec<i32> = vec![1, 2, 3];
// ERROR: format! không phải const
const MSG: &str = format!("hello").as_str();
Lý do: tất cả những hàm trên cần allocate heap memory hoặc gọi std::alloc::alloc — không thể chạy trong compile-time evaluator. Rust 2024 edition đã mở rộng đáng kể tập const fn (nhiều method trên Option, Result, slice, integer giờ là const fn), nhưng vec!, format!, HashMap::new, String::from vẫn ngoài tầm. Heap allocation tại compile time là một feature đang preview qua nightly (#![feature(const_heap)]) — chưa stable.
Mẹo: khi cần "lazy global" cho HashMap/Vec, dùng static kết hợp std::sync::LazyLock (stable từ Rust 1.80) — sẽ xem ở bài 29.
Khác Biệt let — Bảng So Sánh
Tổng hợp 8 khác biệt cốt lõi giữa const và let:
| Khía cạnh | const | let |
|---|---|---|
| Cú pháp | const NAME: T = ...; |
let name: T = ...; (type optional) |
| Naming convention | UPPER_SNAKE_CASE | snake_case |
| Type annotation | Bắt buộc | Optional (infer được) |
| Expression | Compile-time const expression | Runtime expression bất kỳ |
| Scope cho phép | Module / function / block | Chỉ trong function / block |
| Mutability | Luôn immutable | Immutable mặc định, opt-in mut |
| Memory layout | Inline tại mỗi use-site (không có address ổn định) | Stack / heap binding có address |
| Public visibility | pub const expose ra ngoài module |
Không (let chỉ trong fn, không có pub) |
Đọc bảng theo logic: let là runtime binding — đến khi function chạy mới có giá trị, value được lưu vào memory (stack hoặc heap) và có địa chỉ. const là compile-time constant — value được "khắc" vào source code, không tồn tại dạng "biến chạy runtime"; mỗi nơi dùng MAX_SIZE trong code thì compiler thay bằng literal 100 như macro text-replace.
Cùng là "không thay đổi", nhưng let immutable là runtime immutable (đến khi chạy mới biết value, nhưng đã bind thì không sửa được), còn const là compile-time fixed (đã có sẵn value ngay từ trước khi binary chạy).
Const Có Thể Khai Báo Ở Module Scope
Khác biệt practical lớn nhất: const được khai báo ngoài function — ở level module hoặc crate root. let tuyệt đối không. Đây là lý do dùng const để định nghĩa "hằng số toàn cục":
// Module scope - level crate root
const APP_VERSION: &str = "1.0.0";
pub const DEFAULT_PORT: u16 = 8080;
pub const MAX_CONNECTIONS: usize = 1024;
// const trong module con
mod config {
pub const TIMEOUT_SECS: u64 = 30;
pub const RETRY_LIMIT: u32 = 3;
// const trong impl block cũng được
pub struct Server;
impl Server {
pub const NAME: &'static str = "blogcode-api";
}
}
// const trong function body cũng OK (nhưng ít dùng)
fn process() {
const BUFFER_SIZE: usize = 4096;
let buf = [0u8; BUFFER_SIZE];
// ...
}
fn main() {
println!("Version: {APP_VERSION}");
println!("Port: {DEFAULT_PORT}");
println!("Timeout: {}", config::TIMEOUT_SECS);
println!("Server: {}", config::Server::NAME);
}
Ngược lại, viết let X = 5; ngoài function sẽ compile error: expected item, found keyword `let`. Lý do: let là statement, chỉ tồn tại bên trong block { ... } của function/closure/expression-block; còn const là item — cùng category với fn, struct, enum, mod — được phép xuất hiện ở mọi nơi có thể chứa item.
Const ở module scope hoạt động như biến global read-only: mọi function trong module/crate truy cập trực tiếp qua tên (hoặc qua path nếu pub và đặt ở module khác). Vì giá trị inline tại use-site (xem mục 7), không phát sinh runtime cost so với đọc một biến local.
Khi nào dùng pub const: muốn expose hằng số ra ngoài module để consumer dùng — vd lib config (pub const DEFAULT_TIMEOUT_MS: u64 = 5000;), default value cho struct field, sentinel value cho enum-like API.
Const Vs JavaScript const
Người mới đến từ JS rất hay nhầm — cùng tên const nhưng ngữ nghĩa khác hẳn. JS const chỉ ràng buộc binding immutable: tên không thể trỏ đến reference mới. Nhưng value bên trong vẫn mutable nếu là object/array:
// JavaScript (so sánh)
// const obj = { count: 0 };
// obj.count = 100; // OK! value mutate được
// obj = {}; // ERROR: cannot reassign const binding
//
// const PI = 3.14; // không bắt buộc UPPER_CASE
// const fetchUser = async () => { ... }; // bind cả function
// Rust const tương ứng:
// 1) Phải là compile-time constant
// 2) Phải UPPER_SNAKE_CASE (clippy/compiler warn nếu sai)
// 3) Phải có type annotation tường minh
// 4) KHÔNG được giá trị động (object, async fn, runtime expr)
const PI: f64 = 3.14; // OK
// const FETCH_USER = async || { ... }; // ERROR: closure không phải const
// JavaScript dùng const cho biến local trong function, runtime value:
// const userId = await getUserId();
// Rust tương đương:
fn handler() {
let user_id = get_user_id(); // let, không phải const!
println!("{user_id}");
}
fn get_user_id() -> u32 { 42 }
Bảng so sánh nhanh:
- JS
const= "bind immutable, value mutable" → tương đương Rustlet(khôngmut) cho local variable, hoặcstaticcho global runtime value. - JS
let= "bind mutable" → tương đương Rustlet mut. - Rust
const= "compile-time constant + immutable + type required" → JS không có khái niệm tương đương trực tiếp; gần nhất làconst PI = 3.14;ở top-level module nhưng JS không enforce compile-time evaluability.
Quy tắc nhớ khi migrate từ JS sang Rust: mặc định dùng let cho mọi biến local. Chỉ dùng const khi: (1) value biết tại compile time, (2) thực sự là hằng số (magic number, threshold), (3) cần khai báo ở module scope. Còn lại dùng let — Rust ép immutable mặc định nên đã đủ "const-ness" theo nghĩa JS.
Const Inline — Không Có Địa Chỉ Cố Định
Đây là điểm tinh tế nhất của const — và là lý do static (bài 29) phải tồn tại song song.
Khi compiler gặp một use-site của const X, nó inline value ngay tại vị trí đó, như macro text-replacement. Không có một ô memory duy nhất chứa X; mỗi lần dùng, value được "copy" vào context tại chỗ đó. Hệ quả: const không có địa chỉ memory ổn định.
const MAX: i32 = 100;
static MAX_S: i32 = 100;
fn main() {
// Lấy address của const - mỗi lần có thể khác (hoặc bị optimize đi)
let addr1 = &MAX as *const i32;
let addr2 = &MAX as *const i32;
// addr1 và addr2 KHÔNG cam kết bằng nhau
// Lấy address của static - luôn bằng nhau, ổn định
let saddr1 = &MAX_S as *const i32;
let saddr2 = &MAX_S as *const i32;
// saddr1 == saddr2 đảm bảo
println!("const addresses: {:p} {:p}", addr1, addr2);
println!("static addresses: {:p} {:p}", saddr1, saddr2);
}
Cụ thể: khi viết &MAX, compiler tạo một temporary value ở stack frame hiện tại rồi trả reference tới temporary đó. Mỗi use-site → một temporary khác → địa chỉ khác (hoặc đôi khi optimizer hợp nhất, nhưng không có cam kết).
Ảnh hưởng thực tế:
- FFI (Foreign Function Interface): khi gọi C function cần một con trỏ ổn định tới buffer hằng → KHÔNG dùng
constđược, phải dùngstatic. - Pattern matching:
match x { MAX => ... }hoạt động vì compiler so sánh value (inline literal), không phải so sánh address. - Thread sharing:
constkhông phải "shared global" — vì không có một memory location chung. Muốn share state thật giữa thread, dùngstatic.
Đây chính là lý do Bài 29: static Variable phải tồn tại: khi cần một biến global có địa chỉ ổn định trong memory (singleton, FFI buffer, atomic counter), const không đáp ứng được — phải dùng static.
Khi Nào Dùng Const
Sweet spot của const: giá trị biết chắc tại compile time, không cần địa chỉ ổn định, và được dùng nhiều nơi trong code.
// 1) Magic number - thay literal rải rác bằng tên có ý nghĩa
const MAX_RETRIES: u32 = 3;
const TIMEOUT_MS: u64 = 5_000;
const BUFFER_SIZE: usize = 64 * 1024;
// 2) Mathematical / physical constant
const PI: f64 = 3.14159265358979;
const E: f64 = 2.71828182845904;
const SPEED_OF_LIGHT_M_S: u64 = 299_792_458;
// 3) Threshold / limit
const MIN_PASSWORD_LEN: usize = 8;
const MAX_UPLOAD_BYTES: usize = 10 * 1024 * 1024; // 10 MB
const RATE_LIMIT_PER_MIN: u32 = 60;
// 4) Default config
const APP_NAME: &str = "blogcode";
const DEFAULT_HOST: &str = "127.0.0.1";
const DEFAULT_PORT: u16 = 8080;
// 5) Conversion factor
const SECONDS_PER_DAY: u64 = 60 * 60 * 24;
const BYTES_PER_GB: u64 = 1024 * 1024 * 1024;
// Use case thực tế
fn retry_request() {
for attempt in 0..MAX_RETRIES {
println!("Lần thử {}/{MAX_RETRIES}", attempt + 1);
}
}
fn area_of_circle(r: f64) -> f64 {
PI * r * r
}
Mẫu áp dụng: thay vì rải 3, 5000, 8080 khắp codebase, định nghĩa thành const ở đầu module hoặc trong module config. Đổi giá trị về sau chỉ cần sửa 1 chỗ. Đọc code thấy MAX_RETRIES rõ nghĩa hơn nhiều 3.
Quy tắc clippy hỗ trợ: lint identity_op, approx_constant sẽ gợi ý dùng const cho literal magic. Lint large_const_arrays cảnh báo khi const có array quá lớn (gây binary phình to do inline mọi nơi) — trường hợp đó nên đổi qua static.
Anti-Pattern Tránh
Một số sai lầm phổ biến khi dùng const:
1) Dùng const cho String
String là heap-allocated, cấp phát tại runtime — không phải const-evaluable. Code sai:
// SAI - không compile
// const APP_NAME: String = String::from("blogcode");
// ĐÚNG - dùng &'static str
const APP_NAME: &str = "blogcode";
// Nếu cần String thật, convert tại use-site:
fn greet() {
let name: String = APP_NAME.to_string();
println!("Hello {name}");
}
String literal trong nháy kép có type &'static str — sống cùng binary, không cần allocate, hoàn toàn phù hợp với const.
2) Dùng const cho Vec / HashMap
Tương tự, collection growable cần heap → không thể const. Code sai:
use std::collections::HashMap;
// SAI - không compile
// const ALLOWED_USERS: Vec<&str> = vec!["alice", "bob"];
// const STATUS_MAP: HashMap<u16, &str> = HashMap::new();
// ĐÚNG 1 - dùng array nếu kích thước cố định
const ALLOWED_USERS: &[&str] = &["alice", "bob", "charlie"];
const STATUS_CODES: [(u16, &str); 3] = [
(200, "OK"),
(404, "Not Found"),
(500, "Server Error"),
];
// ĐÚNG 2 - dùng static với LazyLock nếu cần collection thực
use std::sync::LazyLock;
static STATUS_MAP: LazyLock<HashMap<u16, &str>> = LazyLock::new(|| {
HashMap::from([
(200, "OK"),
(404, "Not Found"),
(500, "Server Error"),
])
});
fn lookup(code: u16) -> Option<&'static str> {
STATUS_MAP.get(&code).copied()
}
LazyLock (stable từ Rust 1.80) hoặc crate once_cell giải quyết được nhu cầu "static initialize lần đầu khi access" — sẽ chi tiết ở bài 29.
3) Const dài hàng trăm dòng
Vì compiler inline value tại mỗi use-site, một const có array khổng lồ sẽ phình binary mỗi lần dùng. Code khó đọc. Anti-pattern:
// XẤU - const array 10000 phần tử inline mọi use-site
// const LOOKUP_TABLE: [u32; 10000] = [/* ... 10000 số ... */];
// TỐT - static array có address ổn định, không inline
static LOOKUP_TABLE: [u32; 10000] = [/* ... */ 0; 10000];
Quy tắc thực dụng: const cho scalar và collection nhỏ (vài chục phần tử trở lại); table lớn dùng static. Đo binary size trước/sau (qua cargo bloat) để có quyết định cụ thể.
4) Đặt const trong function body chỉ vì "muốn const-ness"
Const trong function body hoạt động, nhưng nếu giá trị chỉ dùng trong function đó và không cần const expression thì let bình thường đã đủ — Rust immutable mặc định rồi. Đừng dùng const chỉ để "trông giống JS".
Tổng Kết
consttrong Rust là compile-time constant, hoàn toàn khácletimmutable — không chỉ là "biến không đổi".- Cú pháp:
const NAME: T = const_expr;với 3 yêu cầu bắt buộc: UPPER_SNAKE_CASE, type annotation tường minh, expression đánh giá được tại compile time. - Const expression cho phép: literal, arithmetic trên const, gọi
const fn, tuple/array/struct với field const. KHÔNG cho phép:String::from,Vec::new,HashMap::new,vec!,format!(heap allocation). constkhai báo được ở module/crate scope vớipub constđể expose;letchỉ trong function body.- JS
const= "bind immutable, value mutable" → tương đương Rustlet, KHÔNG phải Rustconst. Rustconstmạnh hơn nhiều: compile-time + type required. - Compiler inline value của const tại mỗi use-site → không có địa chỉ memory ổn định. Cần address ổn định (FFI, singleton) → dùng
static. - Khi nào dùng: magic number, threshold, default config, mathematical constant, conversion factor. Khi nào tránh: type cần heap (String/Vec/HashMap), array khổng lồ (binary bloat).
- Thay thế khi không dùng được const:
&'static strcho string, array literal cho collection nhỏ,static+LazyLockcho collection thực.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Vì sao
const X = 100;không compile được trong Rust trong khilet x = 100;thì OK? Sửa cho đúng. - Đoạn code
const USERS: Vec<&str> = vec!["alice", "bob"];báo lỗi gì khi build? Hai cách viết thay thế hợp lệ là gì? - Đồng nghiệp viết
const PI: f64 = std::f64::consts::PI.sqrt();bị compile error. Nguyên nhân là gì? (Gợi ý: liên quan đến const fn). - Bạn cần một con trỏ ổn định tới buffer 1 KB để truyền cho FFI C function. Dùng
const BUF: [u8; 1024] = [0; 1024];có phù hợp không? Vì sao? Cách thay thế? - So sánh: developer JS viết
const userId = await fetchId();rồi muốn dịch sang Rust nói "tôi sẽ dùngconst USER_ID: i32 = fetch_id();". Sửa lại đúng convention Rust và giải thích.
Đáp án
- Vì
constlà item ở level module, type annotation bắt buộc — compiler không infer.letlà statement trong function body, có inference. Sửa:const X: i32 = 100;(chọn type cụ thể, không để compiler đoán). - Lỗi
cannot call non-const fn `Vec::<T>::new`(vec! macro expand thành Vec::new + push). Hai cách thay thế: (a) dùng array literal trong sliceconst USERS: &[&str] = &["alice", "bob"];; (b) dùng static với LazyLockstatic USERS: LazyLock<Vec<&str>> = LazyLock::new(|| vec!["alice", "bob"]);. f64::sqrtkhông phảiconst fn— không thể đánh giá ở compile time (cần floating-point hardware operation, định nghĩa qua intrinsic chưa const). Fix: tính sẵn giá trị thủ côngconst SQRT_PI: f64 = 1.7724538509055159;hoặc đợi sqrt thành const fn ở phiên bản rustc mới.- Không phù hợp.
constinline value tại mỗi use-site → không có địa chỉ memory ổn định. C function nhận con trỏ sẽ thấy address khác nhau mỗi lần (hoặc address trỏ vào stack temporary đã out of scope). Cách đúng:static BUF: [u8; 1024] = [0; 1024];— static có một memory location duy nhất, address ổn định suốt program lifetime. - JS
const userId = await fetchId();là runtime value (biến local immutable) — Rust tương đương dùnglet:let user_id = fetch_id();hoặclet user_id = fetch_id().await;. KHÔNG dùngconstvì (a)fetch_id()là runtime function, không phải const expression; (b) đây là biến local trong function, không cần module scope; (c) Rustletđã immutable mặc định, đủ "const-ness" theo nghĩa JS. Đặtconstở đây sẽ compile errorattempt to use a non-constant value in a constant.
Bài Tiếp Theo
Bài 29: static Variable — Biến Toàn Cục Trong Rust — đi sâu vào static: khác biệt với const ở chỗ có địa chỉ memory ổn định, 'static lifetime, static mut tại sao là unsafe, và pattern LazyLock / once_cell để khởi tạo lazy global thread-safe — giải pháp cho mọi trường hợp const không đáp ứng được ở bài này.
