Cách sửa lỗi Rust E0015: Tại sao không thể gọi hàm trong hằng số

intermediate🦀 Rust2026-06-28| Rust (Stable 1.80+), Cargo, mọi hệ điều hành

Error Message

error[E0015]: calls in constants are limited to constant functions, tuple structs and tuple variants
#rust#lỗi-trình-biên-dịch#const#lazylock

Vấn đềKhi chuyển từ các ngôn ngữ như Python hay JavaScript, bạn có thể mong đợi khởi tạo một bản đồ cấu hình toàn cục (global configuration map) bằng cách gọi một hàm khởi tạo đơn giản. Tuy nhiên, Rust bảo vệ quá trình biên dịch chặt chẽ hơn nhiều. Nó phân biệt rạch ròi giữa những gì xảy ra khi bạn xây dựng mã nguồn (build) và những gì xảy ra khi chương trình chạy (runtime).

Dưới đây là đoạn mã thường gây ra rắc rối này:

use std::collections::HashMap;

// Điều này sẽ kích hoạt lỗi E0015
const MY_CONFIG: HashMap = HashMap::new();

fn main() {
    println!("{:?}", MY_CONFIG);
}

Chạy lệnh cargo build, và trình biên dịch sẽ dừng bạn lại với thông báo sau:

error[E0015]: calls in constants are limited to constant functions, tuple structs and tuple variants
 --> src/main.rs:4:39
  |
4 | const MY_CONFIG: HashMap = HashMap::new();
  |                                       ^^^^^^^^^^^^^^
  |
  = note: calls in constants are limited to constant functions, tuple structs and tuple variants

Tại sao điều này xảy raTừ khóa const yêu cầu Rust tính toán một giá trị một lần duy nhất tại thời điểm biên dịch và chèn nó trực tiếp vào mọi nơi nó được sử dụng. Để thực hiện việc này một cách an toàn, trình biên dịch yêu cầu bất kỳ hàm nào được gọi trong khối const phải được đánh dấu là một const fn. Đây là các hàm đơn định (deterministic) mà trình biên dịch có thể thực thi mà không cần chạy toàn bộ chương trình của bạn.

Bạn có thể thắc mắc tại sao HashMap::new() không phải là một const fn. Lý do là vì HashMap sử dụng một hạt giống ngẫu nhiên (random seed) cho thuật toán băm của nó để ngăn chặn các cuộc tấn công DoS. Việc tạo ra sự ngẫu nhiên đó yêu cầu tương tác với hệ điều hành, điều mà trình biên dịch không thể thực hiện khi đang xây dựng tệp thực thi của bạn.

Giải pháp 1: Sử dụng LazyLock (Tiêu chuẩn hiện đại)Nếu bạn đang sử dụng Rust 1.80 hoặc mới hơn, std::sync::LazyLock là lựa chọn tốt nhất. Nó cho phép bạn định nghĩa một biến toàn cục sẽ đợi để khởi tạo cho đến thời điểm chính xác khi bạn truy cập nó lần đầu tiên. Điều này giúp vượt qua các hạn chế tại thời điểm biên dịch trong khi vẫn đảm bảo an toàn luồng (thread-safe).

use std::collections::HashMap;
use std::sync::LazyLock;

// LazyLock chỉ khởi tạo khi MY_CONFIG được sử dụng lần đầu tiên
static MY_CONFIG: LazyLock> = LazyLock::new(|| {
    let mut m = HashMap::new();
    m.insert("api_version", 2);
    m.insert("timeout_ms", 5000);
    m
});

fn main() {
    // HashMap thực sự được tạo tại đây
    println!("Timeout: {:?}", MY_CONFIG.get("timeout_ms"));
}

Lưu ý rằng chúng ta đã chuyển từ const sang static. Điều này đảm bảo dữ liệu nằm ở một vị trí bộ nhớ cố định, duy nhất thay vì bị sao chép ở mọi nơi.

Giải pháp 2: Sử dụng OnceLock cho dữ liệu độngĐôi khi bạn không biết giá trị của biến toàn cục cho đến khi chương trình đang chạy—có lẽ bạn đang đọc một DATABASE_URL từ biến môi trường. Trong những trường hợp này, std::sync::OnceLock là sự lựa chọn tốt hơn.

use std::collections::HashMap;
use std::sync::OnceLock;

static APP_CACHE: OnceLock> = OnceLock::new();

fn main() {
    // Khởi tạo cache một lần duy nhất tại thời điểm chạy
    let cache = APP_CACHE.get_or_init(|| {
        let mut m = HashMap::new();
        m.insert("session_id".to_string(), "abc-123".to_string());
        m
    });

    println!("Current session: {}", cache["session_id"]);
}

Giải pháp 3: Chuyển đổi các hàm tùy chỉnh sang 'const'Nếu bạn gặp lỗi E0015 khi gọi hàm của chính mình, bạn thường có thể khắc phục bằng cách thêm từ khóa const. Điều này hoạt động miễn là hàm của bạn không thực hiện cấp phát bộ nhớ heap hoặc các thao tác I/O phức tạp.

// Thêm 'const' cho phép hàm này chạy trong quá trình biên dịch
const fn get_max_threads(cores: u32) -> u32 {
    cores * 2
}

const WORKER_LIMIT: u32 = get_max_threads(8);

fn main() {
    println!("Max workers: {}", WORKER_LIMIT);
}

Giải pháp 4: Sử dụng mảng tĩnh để không tốn chi phí vận hành (Zero Overhead)Đối với các tra cứu nhỏ và cố định, một HashMap có thể là quá mức cần thiết. Một mảng tĩnh gồm các bản ghi (tuple) thường nhanh hơn và trình biên dịch dễ xử lý hơn nhiều. Cách tiếp cận này không tốn thời gian khởi tạo khi chạy chương trình.

// Một mảng đơn giản gồm các cặp thường là đủ cho các hằng số
const SERVER_SETTINGS: [(&str, u16); 3] = [
    ("web", 8080),
    ("api", 9000),
    ("metrics", 9100),
];

fn main() {
    for (service, port) in SERVER_SETTINGS {
        println!("Service {} is on port {}", service, port);
    }
}

Tóm tắt & Phòng ngừa- Tránh các kiểu dữ liệu heap trong const: Đừng sử dụng String, Vec, hoặc HashMap trực tiếp trong một const.- Ưu tiên sử dụng LazyLock: Đây là cách sạch sẽ nhất để xử lý trạng thái toàn cục trong Rust hiện đại.- Kiểm tra tài liệu: Nếu một hàm không được đánh dấu rõ ràng là const fn trong thư viện chuẩn, bạn không thể sử dụng nó trong khai báo const.- Mẹo hiệu suất: Nếu bạn cần một bản đồ bất biến (immutable map) hiệu suất cao tại thời điểm biên dịch, hãy tìm hiểu crate phf (Perfect Hash Function).

Related Error Notes