Bỏ qua để đến nội dung

Lỗi Rust E0434: can't capture dynamic environment in a fn item (biến cục bộ)

E0434 nghĩa là một hàm được viết bên trong một hàm khác đã cố dùng một biến cục bộ của hàm bên ngoài. Trông có vẻ như phải chạy được: biến nằm ngay đó, cách vài dòng phía trên. Nhưng trong Rust, một fn lồng bên trong là một mục (item) riêng biệt, và nó không nhìn thấy các biến xung quanh. Cách diễn đạt của trình biên dịch, “dynamic environment” (môi trường động), chỉ có nghĩa là “các biến cục bộ tồn tại trong lúc hàm bên ngoài chạy”. Đây là một trong những lỗi dễ sửa nhất: rustc nói luôn đáp án trong dòng help:. (Trang này thuộc loạt bài Giải thích các lỗi trình biên dịch Rust.)

error[E0434]: can't capture dynamic environment in a fn item
--> src/main.rs:5:24
|
5 | price * (1.0 + tax_rate) // ❌ inner `fn` can't see `tax_rate`
| ^^^^^^^^
|
= help: use the `|| { ... }` closure form instead

Thông báo của trình biên dịch luôn bằng tiếng Anh: “can’t capture dynamic environment in a fn item” nghĩa là “một mục fn không thể bắt (capture) môi trường động”. Đây là chương trình gây ra lỗi đó (chú thích được giữ bằng tiếng Anh vì nó xuất hiện nguyên văn trong thông báo lỗi):

fn main() {
let tax_rate = 0.2;
fn with_tax(price: f64) -> f64 {
price * (1.0 + tax_rate) // ❌ inner `fn` can't see `tax_rate`
}
println!("{:.2}", with_tax(10.0));
}

Dấu ^^^ chỉ vào tax_rate ở dòng 5. Đó là một biến cục bộ của main, được tạo bằng let ở dòng 2. Hàm bên trong with_tax cố đọc nó, và điều đó không được phép.

Dòng help: đưa ra cách sửa: hãy viết một closure, || { ... }, thay cho một fn lồng. Closure có thể dùng các biến xung quanh nó. Còn fn thì không, dù bạn viết nó ở đâu.

Bạn cũng thường gặp thêm một cảnh báo, unused variable: `tax_rate` (biến không được dùng), vì theo góc nhìn của Rust, chẳng có gì được phép dùng nó. Cảnh báo đó biến mất cùng với lỗi.

Giống như một lá thư dán kín mà bạn viết ở bàn làm việc. Việc những tờ ghi chú nằm trên bàn lúc bạn viết chẳng có ý nghĩa gì; người mở thư chỉ thấy những gì bạn bỏ vào phong bì.

Một fn lồng được biên dịch y hệt như một fn ở đầu file. Viết nó bên trong main chỉ giấu tên của nó khỏi phần code còn lại. Nó không nối hàm đó với các biến của main, và có lý do chính đáng: một hàm bình thường không có chỗ nào để giữ chúng. Bạn có thể gọi with_tax từ bất cứ đâu mà main chạm tới được, lưu nó dưới dạng con trỏ hàm, hoặc gọi nó sau khi các biến của main đã biến mất. Một fn bình thường chỉ là code, không kèm theo dữ liệu nào.

Closure là code cộng với các biến mà nó đã bắt. Rust lưu những giá trị đó (hoặc tham chiếu tới chúng) bên trong closure, và borrow checker đảm bảo chúng sống đủ lâu. Đó là lý do rustc chỉ bạn tới closure. Xem Bắt biến từ phạm vi xung quanh.

Quy tắc tương tự áp dụng cho tham số generic: một fn bên trong cũng không dùng được T của hàm bên ngoài. Trường hợp đó là E0401.

Phần lớn trường hợp, hãy chọn cách này, khi hàm hỗ trợ nhỏ và chỉ nằm trong một hàm. Viết let name = |params| body; và closure có thể dùng bất kỳ biến nào trong phạm vi:

fn main() {
let tax_rate = 0.2;
let with_tax = |price: f64| price * (1.0 + tax_rate); // ✅ closure nhìn thấy `tax_rate`
println!("{:.2}", with_tax(10.0));
println!("{:.2}", with_tax(24.5));
let prices = [3.0, 7.5, 12.0];
let total: f64 = prices.iter().map(|&p| with_tax(p)).sum();
println!("tổng: {:.2}", total);
}

Kết quả:

12.00
29.40
tổng: 27.00
▶ Tự chạy thử

Một closure chỉ đọc biến thì mượn biến đó, nên bạn vẫn dùng được tax_rate sau đó. Nếu bạn thay đổi biến bên trong closure, closure cần let mut. Xem Closure.

Cách sửa 2: Truyền giá trị vào làm đối số

Phần tiêu đề “Cách sửa 2: Truyền giá trị vào làm đối số”

Chọn cách này khi hàm hỗ trợ nên tiếp tục là một hàm thật sự: nó dài hơn, bạn muốn viết test cho nó, hoặc nhiều hàm cùng dùng nó. Hãy cho nó thêm một tham số và truyền giá trị vào:

fn with_tax(price: f64, tax_rate: f64) -> f64 {
price * (1.0 + tax_rate) // ✅ giờ `tax_rate` là một tham số
}
fn receipt(prices: &[f64], tax_rate: f64) -> f64 {
prices.iter().map(|&p| with_tax(p, tax_rate)).sum()
}
fn main() {
let tax_rate = 0.2;
println!("{:.2}", with_tax(10.0, tax_rate));
println!("tổng: {:.2}", receipt(&[3.0, 7.5, 12.0], tax_rate));
let reduced_rate = 0.05;
println!("{:.2}", with_tax(10.0, reduced_rate));
}

Kết quả:

12.00
tổng: 27.00
10.50
▶ Tự chạy thử

Ở đây hàm đã được chuyển lên cấp cao nhất, nơi phần lớn hàm hỗ trợ trong Rust sinh sống. Giữ nó lồng bên trong cũng được; điều quan trọng là giá trị được đưa vào qua đối số. Thêm một điểm cộng: giờ nó dùng được với bất kỳ mức thuế nào, không chỉ một. Xem Tham số.

Chọn cách này khi giá trị cố định và đã biết trước khi chương trình chạy, như một mức thuế được viết thẳng trong code. Khác với biến let, một const là một mục, và hàm nào cũng dùng được nó, dù lồng hay không:

const TAX_RATE: f64 = 0.2; // ✅ một hằng số, mọi hàm đều nhìn thấy
fn main() {
fn with_tax(price: f64) -> f64 {
price * (1.0 + TAX_RATE)
}
println!("{:.2}", with_tax(10.0));
println!("{:.2}", with_tax(24.5));
}

Kết quả:

12.00
29.40
▶ Tự chạy thử

Cách này chỉ dùng được cho giá trị tính được lúc biên dịch. Nếu mức thuế đến từ file, từ dữ liệu người dùng nhập, hoặc từ một biến khác, const sẽ không làm được: const RATE: f64 = tax_rate; gây ra một lỗi khác, E0435 (attempt to use a non-constant value in a constant). Khi đó hãy dùng cách sửa 1 hoặc cách sửa 2. Xem Hằng số.

Khi bạn đến từ JavaScript hoặc Python. Trong những ngôn ngữ đó, hàm lồng nhìn thấy biến bên ngoài, nên viết fn trong fn có vẻ tự nhiên. Trong Rust, thứ làm được điều đó là closure. Cách sửa: let helper = |x| ...; thay cho fn helper(x) ....

Dùng self trong một fn lồng bên trong phương thức. fn report(&self) { fn show() { println!("{}", self.n); } } cho ra cùng lỗi E0434, chỉ vào self. Cách sửa: biến show thành một phương thức (fn show(&self) trong impl) và gọi self.show(), hoặc dùng closure.

Một tham số hàm, không phải let. Trong fn total(prices: &[f64], discount: f64), một fn apply(price: f64) -> f64 { price - discount } lồng bên trong cũng gây ra E0434: tham số cũng là biến cục bộ. Cách sửa: truyền discount vào apply, hoặc viết let apply = |price: f64| price - discount;.

Kiểm tra nhanh

1. Bên trong main, bạn có let limit = 10; và một fn check(n: i32) -> bool { n < limit } lồng bên trong. Chuyện gì xảy ra?

2. Phiên bản nào dùng được biến cục bộ limit?

3. Giá trị đến từ dữ liệu người dùng nhập lúc chạy. Cách sửa nào sẽ KHÔNG dùng được?

Điểm: 0 / 3