Danh sách bài viết

Bài 28: const vs let — Hai Khái Niệm Khác Nhau

Bài 28 của series Rust Cơ Bản — nhiều người mới đến từ JavaScript hay C++ tưởng const trong Rust chỉ là "biến không đổi" — sai. const trong Rust là compile-time constant, hoàn toàn khác let immutable: bắt buộc UPPER_SNAKE_CASE, bắt buộc type annotation, expression phải đánh giá được tại compile time, khai báo được ở module scope (level crate/global), inline value tại mỗi use-site nên không có địa chỉ memory ổn định. Bài này mổ xẻ 8 khác biệt cốt lõi và chỉ rõ khi nào dùng const, khi nào tránh.

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ẽ:

  • 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 khi const X: i32 = 1 + 2; thì OK.
  • Phân biệt rõ const với let qua 8 tiêu chí: scope, naming, type annotation, expression, mutability, memory layout, visibility, runtime semantics.
  • Hiểu vì sao const trong Rust khác hoàn toàn const của JavaScript dù cùng tên.
  • Biết Rust inline value của const tại mỗi use-site → không có địa chỉ memory ổn định → preview lý do tồn tại static ở 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.

2

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_acronyms và compiler emit warning non_upper_case_globals nếu vi phạm. Mục đích: ở bất kỳ chỗ nào trong code thấy MAX_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ỗi missing 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ọi const 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ì.

3

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ấu const fn tứ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.

4

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 constlet:

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: letruntime 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ỉ. constcompile-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 constcompile-time fixed (đã có sẵn value ngay từ trước khi binary chạy).

5

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 constitem — 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.

6

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 Rust let (không mut) cho local variable, hoặc static cho global runtime value.
  • JS let = "bind mutable" → tương đương Rust let 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.

7

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ùng static.
  • 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: const không phải "shared global" — vì không có một memory location chung. Muốn share state thật giữa thread, dùng static.

Đâ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.

8

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.

9

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".

10

Tổng Kết

  • const trong Rust là compile-time constant, hoàn toàn khác let immutable — 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).
  • const khai báo được ở module/crate scope với pub const để expose; let chỉ trong function body.
  • JS const = "bind immutable, value mutable" → tương đương Rust let, KHÔNG phải Rust const. Rust const mạ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 str cho string, array literal cho collection nhỏ, static + LazyLock cho collection thực.
11

Bài Tập Củng Cố

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

  1. Vì sao const X = 100; không compile được trong Rust trong khi let x = 100; thì OK? Sửa cho đúng.
  2. Đ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ì?
  3. Đồ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).
  4. 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ế?
  5. 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ùng const USER_ID: i32 = fetch_id();". Sửa lại đúng convention Rust và giải thích.
Đáp án
  1. const là item ở level module, type annotation bắt buộc — compiler không infer. let là statement trong function body, có inference. Sửa: const X: i32 = 100; (chọn type cụ thể, không để compiler đoán).
  2. 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 slice const USERS: &[&str] = &["alice", "bob"];; (b) dùng static với LazyLock static USERS: LazyLock<Vec<&str>> = LazyLock::new(|| vec!["alice", "bob"]);.
  3. f64::sqrt không phải const 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ông const SQRT_PI: f64 = 1.7724538509055159; hoặc đợi sqrt thành const fn ở phiên bản rustc mới.
  4. Không phù hợp. const inline 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.
  5. JS const userId = await fetchId();runtime value (biến local immutable) — Rust tương đương dùng let: let user_id = fetch_id(); hoặc let user_id = fetch_id().await;. KHÔNG dùng const vì (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) Rust let đã immutable mặc định, đủ "const-ness" theo nghĩa JS. Đặt const ở đây sẽ compile error attempt to use a non-constant value in a constant.
12

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.