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

Lỗi Rust E0424: expected value, found module `self` (dùng self ngoài phương thức)

E0424 nghĩa là bạn viết self bên trong một hàm không có tham số self, nên chẳng có “self” nào để Rust trỏ tới. Đây là một sơ suất rất hay gặp, nhất là khi bạn đến từ Java, C# hay JavaScript, nơi this lúc nào cũng có sẵn. Cách sửa thường chỉ là một từ: thêm &self vào danh sách tham số của hàm. Trang này thuộc hướng dẫn về lỗi trình biên dịch.

error[E0424]: expected value, found module `self`
--> src/main.rs:7:32
|
6 | fn describe() {
| -------- this function doesn't have a `self` parameter
7 | println!("Player: {}", self.name); // ❌ which player?
| ^^^^ `self` value is a keyword only available in methods with a `self` parameter
|
help: add a `self` receiver parameter to make the associated `fn` a method
|
6 | fn describe(&self) {
| +++++
error[E0599]: no method named `describe` found for struct `Player` in the current scope
--> src/main.rs:13:10
|
13 | hero.describe();
| ^^^^^^^^ this is an associated function, not a method

Trình biên dịch báo rằng self chỉ dùng được trong phương thức có tham số self, và gợi ý thêm &self vào describe.

Đây là chương trình gây ra lỗi:

struct Player {
name: String,
}
impl Player {
fn describe() {
println!("Player: {}", self.name); // ❌ which player?
}
}
fn main() {
let hero = Player { name: String::from("Ferris") };
hero.describe();
}

Chú thích // ❌ which player? (người chơi nào?) giữ tiếng Anh vì nó xuất hiện trong thông báo lỗi. Lỗi thứ hai, E0599, là hệ quả của lỗi thứ nhất. Sửa E0424 là nó cũng biến mất.

Hãy nhìn hai nhãn mà rustc gắn lên code của bạn:

  1. Dòng 6: fn describe() có cặp ngoặc rỗng. rustc ghi chú “this function doesn’t have a self parameter” (hàm này không có tham số self).
  2. Dòng 7: self.name đòi tên của “người chơi mà hàm này được gọi trên đó”. Nhưng describe không được trao người chơi nào, nên self ở đây chẳng có nghĩa gì.

Một hàm nằm trong khối impl mà không có tham số self là một hàm liên kết (associated function). Nó thuộc về kiểu (Player::describe()), không thuộc về một giá trị cụ thể nào. Một hàm có tham số self là một phương thức (method), và chỉ phương thức mới nói self được.

Hãy nghĩ tới một tờ đơn ghi “hãy viết tên của bạn vào đây”. Nó chỉ có nghĩa khi có ai đó đang cầm tờ đơn. describe() là tờ đơn chẳng ai cầm.

Vì sao nó lại nói “found module self”? Trong Rust, self còn có nghĩa thứ hai: module hiện tại, như trong use self::helpers;. Khi không có giá trị self nào, rustc quay về nghĩa đó rồi phàn nàn rằng một module không phải là giá trị. Bạn có thể bỏ qua chữ “module”; vấn đề thật là thiếu tham số.

Trong Rust, không có gì bị giấu. Java và C# cho mọi phương thức không static một this vô hình. Rust bắt bạn viết self làm tham số đầu tiên, và bạn còn nói rõ phương thức nhận nó theo cách nào: chỉ nhìn, thay đổi, hay lấy quyền sở hữu. Lựa chọn đó là một phần của quy tắc mượn (borrowing), nên nó phải hiện ra rõ ràng.

Nhờ vậy, mọi người đọc code của bạn, và cả trình biên dịch, biết chính xác một phương thức làm gì với giá trị của nó. fn summary(&self) hứa không thay đổi gì. fn add(&mut self, ...) báo trước rằng nó thay đổi. fn checkout(self) dùng hết giá trị. Rồi trình biên dịch kiểm tra các lời hứa đó cho bạn. Phương thức và khối impl được giải thích ở Struct.

Chọn cách này khi hàm được dùng để làm việc trên một giá trị đã có. Đây là cách sửa đúng trong phần lớn trường hợp. Thêm tham số mà rustc gợi ý và gọi bằng dấu chấm:

struct Player {
name: String,
}
impl Player {
fn describe(&self) {
println!("Người chơi: {}", self.name);
}
}
fn main() {
let hero = Player { name: String::from("Ferris") };
hero.describe();
}

Kết quả:

Người chơi: Ferris
▶ Tự chạy thử

rustc luôn gợi ý &self, nhưng có ba loại. Hãy chọn loại khớp với việc phương thức làm:

Tham số Phương thức… Tên thường gặp
&self chỉ đọc giá trị summary, len, is_empty, describe
&mut self thay đổi giá trị add, push, reset, level_up
self dùng hết giá trị (lấy quyền sở hữu) into_..., finish, build, checkout

Đây là cả ba trên cùng một kiểu:

struct Order {
items: Vec<String>,
}
impl Order {
fn summary(&self) -> String {
format!("{} món: {}", self.items.len(), self.items.join(", "))
}
fn add(&mut self, item: &str) {
self.items.push(item.to_string());
}
fn checkout(self) -> Vec<String> {
self.items
}
}
fn main() {
let mut order = Order { items: Vec::new() };
order.add("cà phê");
order.add("bánh mì");
println!("{}", order.summary());
let packed = order.checkout(); // order bị dùng hết ở đây
println!("Đã đóng gói: {:?}", packed);
}

Kết quả:

2 món: cà phê, bánh mì
Đã đóng gói: ["cà phê", "bánh mì"]
▶ Tự chạy thử

Nếu bạn chọn &self mà phương thức lại thay đổi một trường, trình biên dịch sẽ báo ngay (E0594 “cannot assign to self.count, which is behind a & reference”, hoặc E0596 với các lời gọi như push). Hãy chuyển sang &mut self. Khi phân vân, cứ bắt đầu với &self; self là loại hiếm dùng nhất.

Một khi hàm đã có tham số self, bạn có thể gọi nó theo hai cách. hero.label() và Player::label(&hero) có nghĩa y hệt nhau: giá trị bạn đặt trước dấu chấm trở thành đối số đầu tiên. Cách dùng dấu chấm là cách bạn sẽ viết nhiều nhất. Cách Type:: tiện khi bạn muốn truyền chính phương thức cho thứ gì đó như map:

struct Player {
name: String,
level: u32,
}
impl Player {
fn label(&self) -> String {
format!("{} (cấp {})", self.name, self.level)
}
}
fn main() {
let hero = Player { name: String::from("Ferris"), level: 3 };
println!("{}", hero.label());
println!("{}", Player::label(&hero)); // cùng một lời gọi
let team = vec![hero, Player { name: String::from("Corro"), level: 5 }];
let labels: Vec<String> = team.iter().map(Player::label).collect();
println!("{:?}", labels);
}

Kết quả:

Ferris (cấp 3)
Ferris (cấp 3)
["Ferris (cấp 3)", "Corro (cấp 5)"]
▶ Tự chạy thử

Thứ không chạy được là Player::label() mà không có giá trị: một phương thức luôn cần self của nó. Bỏ nó đi sẽ gây E0061, “this function takes 1 argument but 0 arguments were supplied” (hàm này nhận 1 đối số nhưng được truyền 0).

Chọn cách này khi chưa có giá trị nào, hoặc khi hàm hoàn toàn không nằm trong khối impl.

Trường hợp kinh điển là constructor. new tạo ra giá trị, nên chẳng có self nào để đọc. Hãy đưa dữ liệu ban đầu cho new qua các tham số bình thường và tạo struct bằng Self { ... }:

struct Counter {
count: u32,
step: u32,
}
impl Counter {
fn new(step: u32) -> Self {
Self { count: 0, step }
}
fn tick(&mut self) {
self.count += self.step;
}
}
fn main() {
let mut counter = Counter::new(5);
counter.tick();
counter.tick();
println!("Đếm: {}", counter.count);
}

Kết quả:

Đếm: 10
▶ Tự chạy thử

Để ý chữ hoa: Self (S hoa) là kiểu, nên Self { ... } tạo ra một giá trị mới. self (s thường) là giá trị mà phương thức được gọi trên đó.

Ý tưởng này cũng sửa được một hàm tự do, tức hàm nằm ngoài mọi khối impl. Ở đó rustc diễn đạt hơi khác: “this function can’t have a self parameter” (hàm này không thể có tham số self). Hãy truyền struct vào bằng tham chiếu:

struct Cart {
items: Vec<u32>,
}
fn total(cart: &Cart) -> u32 {
cart.items.iter().sum()
}
// gọi như sau: total(&cart)

Hoặc chuyển total vào impl Cart và cho nó &self, như ở Cách sửa 1. Cả hai đều ổn; dùng phương thức sẽ đọc tự nhiên hơn khi hàm rõ ràng là “về” một giỏ hàng.

Bên trong new. Viết self.count = 0; trong fn new() -> Self gây E0424, và gợi ý fn new(&self) -> Self của rustc sẽ vô lý (bạn phải có một bộ đếm mới tạo được bộ đếm). Cách sửa: tạo giá trị bằng Self { count: 0 }, như ở Cách sửa 3.

Bên trong closure ở một hàm liên kết. Closure không phải là vấn đề; hàm bao quanh nó mới là vấn đề:

error[E0424]: expected value, found module `self`
--> src/main.rs:8:35
|
7 | fn sale_prices(prices: &[u32]) -> Vec<u32> {
| ----------- this function doesn't have a `self` parameter
8 | prices.iter().map(|p| p - self.discount).collect()
| ^^^^ `self` value is a keyword only available in methods with a `self` parameter

Rust báo rằng sale_prices không có tham số self, nên closure bên trong không thể dùng self.discount. Cách sửa: biến nó thành phương thức, fn sale_prices(&self, prices: &[u32]), hoặc truyền con số vào, fn sale_prices(prices: &[u32], discount: u32). Xem Closure.

Trong main hoặc một hàm phụ. self không bao giờ dùng được bên ngoài khối impl. Cách sửa: truyền struct vào làm tham số (total(&cart)), hoặc gọi một phương thức trên nó (cart.total()).

Quen với phương thức “static” của C# hay Java. Một hàm liên kết không có self là phiên bản phương thức static của Rust. Nếu bạn muốn phiên bản dành cho đối tượng, bạn phải tự thêm tham số self. Xem Rust cho lập trình viên C#.

  • E0425: sơ suất ngược lại. Bạn đang ở trong một phương thức, nhưng viết count thay vì self.count.
  • E0599: “no method named … found”. Thường được in ngay sau E0424, vì một hàm không có self không thể gọi bằng dấu chấm.
  • E0061: sai số lượng đối số, ví dụ gọi Player::label() mà không có giá trị.
  • E0596: bạn đã thêm &self, nhưng phương thức thay đổi một trường, nên nó cần &mut self. Gán cho một trường theo cách này sẽ gây E0594.
  • 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 impl Player, fn heal() { self.hp += 10; } gây E0424. Cách sửa tốt nhất là gì?

2. Vì sao fn new() -> Self không dùng được self.name?

3. fn label(&self) -> String được định nghĩa trên Player. Lời gọi nào giống hệt hero.label()?

Điểm: 0 / 3