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

Lỗi Rust E0401: can't use generic parameters from outer item (T của hàm ngoài)

E0401 nghĩa là một hàm được viết bên trong một hàm khác đã cố dùng kiểu generic của hàm bên ngoài, như T, hoặc Self của khối impl chứa nó. Cảm giác như lẽ ra phải chạy được, vì hàm bên trong nằm ngay trong hàm bên ngoài. Nhưng trong Rust, một fn lồng bên trong là một mục (item) riêng biệt và không nhìn thấy T của bên ngoài. Đây là điều bất ngờ hay gặp khi bạn dọn dẹp một hàm generic bằng cách kéo một hàm hỗ trợ vào bên trong nó, và sửa chỉ mất một phút. Trang này thuộc loạt bài về lỗi trình biên dịch.

error[E0401]: can't use generic parameters from outer item
--> src/main.rs:2:20
|
1 | fn print_all<T: std::fmt::Display>(items: &[T]) {
| - type parameter from outer item
2 | fn show(item: &T) { // ❌ inner `fn` can't see the outer `T`
| ---- ^ use of generic parameter from outer item
| |
| generic parameter used in this inner function
|
= note: nested items are independent from their parent item for everything except for privacy and name resolution
help: try introducing a local generic parameter here
|
2 | fn show<T>(item: &T) { // ❌ inner `fn` can't see the outer `T`
| +++

Thông báo của trình biên dịch luôn bằng tiếng Anh: “can’t use generic parameters from outer item” nghĩa là “không thể dùng tham số generic của mục bên ngoài”. Đâ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 print_all<T: std::fmt::Display>(items: &[T]) {
fn show(item: &T) { // ❌ inner `fn` can't see the outer `T`
println!("- {}", item);
}
for item in items {
show(item);
}
}
fn main() {
print_all(&["apples", "pears"]);
}

rustc chỉ vào ba chỗ:

  1. Dấu - dưới T ở dòng 1: “type parameter from outer item” (tham số kiểu của mục bên ngoài). Đây là T thuộc về print_all.
  2. Dấu ^ dưới T ở dòng 2: “use of generic parameter from outer item” (dùng tham số generic của mục bên ngoài). Hàm bên trong show cố dùng T đó.
  3. Dấu ---- dưới show: “generic parameter used in this inner function” (tham số generic bị dùng trong hàm bên trong này). Đây là hàm không được phép làm vậy.

Dòng note: nói quy tắc trong một câu: các mục lồng bên trong độc lập với mục cha của chúng. Đặt fn show bên trong print_all chỉ giấu tên của nó khỏi phần còn lại của file. Nó không cho show quyền truy cập bất cứ thứ gì thuộc về print_all: không có T, không có tham số, không có biến cục bộ.

Giống như phòng dành cho khách trong một căn nhà chung. Căn phòng nằm trong nhà, nhưng người khách không được đưa chìa khóa phòng ngủ của bạn chỉ vì họ ngủ ở cuối hành lang.

Một hàm generic như print_all<T> là một khuôn mẫu. Rust tạo một bản riêng cho mỗi T mà bạn gọi nó: một bản cho &str, một bản cho i32, v.v. Còn một fn lồng bên trong thì được biên dịch một lần, như một mục riêng, y hệt như khi bạn viết nó ở đầu file. Vì vậy bên trong show, không có một T duy nhất nào để nó ám chỉ.

Rust có thể đã làm cho hàm lồng tự nhân bản cùng với hàm cha, nhưng như thế sẽ khiến chúng hoạt động rất khác mọi fn khác. Thay vào đó, quy tắc rất đơn giản: một fn bên trong chỉ biết những gì bạn đưa cho nó, qua tham số generic và đối số của riêng nó. Nếu bạn muốn một thứ có nhìn thấy code xung quanh, Rust có closure cho việc đó. Xem Generic và Closure.

Cách sửa 1: Cho hàm bên trong tham số generic riêng

Phần tiêu đề “Cách sửa 1: Cho hàm bên trong tham số generic riêng”

Phần lớn trường hợp, hãy chọn cách này. Khai báo <T> cho cả hàm bên trong, cùng những ràng buộc mà nó cần. Phần help của rustc gợi ý fn show<T>, nhưng nếu bạn chỉ thêm <T>, bạn sẽ gặp một lỗi mới, E0277 (`T` doesn't implement `std::fmt::Display`), vì T mới chưa có ràng buộc nào. Hãy chép các ràng buộc sang:

use std::fmt::Display;
fn print_all<T: Display>(items: &[T]) {
fn show<T: Display>(item: &T) {
println!("- {}", item);
}
for item in items {
show(item);
}
}
fn main() {
print_all(&["táo", "lê"]);
print_all(&[3, 7]);
}

Kết quả:

- táo
- lê
- 3
- 7
▶ Tự chạy thử

T bên trong là một tham số mới tình cờ có cùng tên. Khi print_all gọi show(item), Rust điền T bên ngoài vào đó. Bạn có thể đặt tên nó là U cho rõ hơn.

Chọn cách này khi hàm hỗ trợ nhỏ, hoặc khi nó cũng cần các biến của hàm bên ngoài. Closure không phải là một mục riêng, nên nó nhìn thấy mọi thứ xung quanh: T bên ngoài, tham số của hàm, và các biến cục bộ. Ở đây closure còn dùng label, thứ mà một fn lồng không bao giờ chạm tới được:

use std::fmt::Display;
fn print_all<T: Display>(label: &str, items: &[T]) {
let show = |item: &T| println!("{}: {}", label, item);
for item in items {
show(item);
}
}
fn main() {
print_all("trái cây", &["táo", "lê"]);
}

Kết quả:

trái cây: táo
trái cây: lê
▶ Tự chạy thử

Closure không thể tự mình là generic, nhưng ở đây cũng không cần: bên trong print_all<T>, &T vốn đã là một kiểu cụ thể cho mỗi bản của hàm. Xem Closure.

Cách sửa 3: Chuyển hàm ra ngoài (hoặc vào impl)

Phần tiêu đề “Cách sửa 3: Chuyển hàm ra ngoài (hoặc vào impl)”

Chọn cách này khi hàm hỗ trợ hữu ích một cách độc lập, hoặc khi lỗi nói về Self. Bên trong một phương thức, một fn lồng cũng không dùng được Self, vì nó nằm ngoài impl theo đúng cách đó:

error[E0401]: can't use `Self` from outer item
--> src/main.rs:7:24
|
5 | impl Inventory {
| ---- `Self` type implicitly declared here, by this `impl`
6 | fn report(&self) {
7 | fn count(inv: &Self) -> usize { // ❌ inner `fn` can't see `Self`
| ----- ^^^^ use of `Self` from outer item
| |
| `Self` used in this inner function
|
= note: nested items are independent from their parent item for everything except for privacy and name resolution
help: refer to the type directly here instead
|
7 - fn count(inv: &Self) -> usize { // ❌ inner `fn` can't see `Self`
7 + fn count(inv: &Inventory) -> usize { // ❌ inner `fn` can't see `Self`
|

Chương trình bị lỗi:

struct Inventory {
items: Vec<String>,
}
impl Inventory {
fn report(&self) {
fn count(inv: &Self) -> usize { // ❌ inner `fn` can't see `Self`
inv.items.len()
}
println!("{} items", count(self));
}
}
fn main() {
let inv = Inventory { items: vec!["rope".to_string()] };
inv.report();
}

Gợi ý của rustc (viết &Inventory) dùng được. Nhưng thường thì cách sửa gọn hơn là chuyển count lên một cấp, để nó trở thành một phương thức riêng. Ở đó, Self và self hoạt động như bình thường:

struct Inventory {
items: Vec<String>,
}
impl Inventory {
fn count(&self) -> usize {
self.items.len()
}
fn report(&self) {
println!("{} món đồ", self.count());
}
}
fn main() {
let inv = Inventory {
items: vec!["dây thừng".to_string(), "đèn".to_string()],
};
inv.report();
}

Kết quả:

2 món đồ
▶ Tự chạy thử

Với một hàm hỗ trợ generic như show, “chuyển ra ngoài” nghĩa là một fn show<T: Display>(item: &T) ở cấp cao nhất, mà các hàm khác cũng dùng lại được. Xem Struct và Hàm trong Rust.

Một struct hoặc enum bên trong hàm generic. fn group<T>(items: Vec<T>) có struct Pair { a: T, b: T } bên trong sẽ báo E0401 ở mỗi chữ T. Cách sửa: cho kiểu bên trong cũng là generic, struct Pair<T> { a: T, b: T }, hoặc chuyển nó lên cấp cao nhất.

Một const phụ thuộc vào T. const SIZE: usize = std::mem::size_of::<T>(); bên trong một hàm generic cũng là E0401, vì const cũng là một mục. Cách sửa: dùng let: let size = std::mem::size_of::<T>();.

Một hàm bên trong dùng biến cục bộ. Đó là cùng ý tưởng nhưng với giá trị thay vì kiểu, và mang mã khác: E0434 (can't capture dynamic environment in a fn item). Cách sửa: truyền giá trị vào làm đối số, hoặc dùng closure (cách sửa 2).

  • E0411: Self bị dùng bên ngoài mọi impl hoặc trait, ví dụ trong một fn new() -> Self ở cấp cao nhất.
  • E0277: ràng buộc trait không được thỏa mãn, thứ bạn gặp sau khi viết fn show<T> mà không chép T: Display.
  • E0425: không tìm thấy một giá trị hoặc kiểu, ví dụ một kiểu bạn chưa bao giờ khai báo.
  • E0107: sai số lượng đối số generic, một sai sót khác liên quan tới generic.
  • E0434: một fn bên trong dùng biến cục bộ của hàm bên ngoài, như ở trên.
  • Tất cả lỗi trình biên dịch: danh sách đầy đủ các trang lỗi.

Kiểm tra nhanh

1. Bên trong fn print_all<T>(...), vì sao một fn show(item: &T) lồng bên trong không dùng được T?

2. Hàm hỗ trợ nào bên trong fn print_all<T: Display> dùng được cả T lẫn các biến cục bộ bên ngoài?

3. Bạn làm theo help của rustc và viết fn show<T>(item: &T), rồi in item bằng {}. Chuyện gì xảy ra?

Điểm: 0 / 3