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

Lỗi Rust E0790: cannot call associated function on trait (gọi hàm trên trait)

E0790 nghĩa là bạn đòi một trait đưa ra thứ mà chỉ một kiểu mới đưa được. Default::default() đứng một mình giống như nói “cho tôi giá trị mặc định” mà không nói mặc định của cái gì. Đây là lỗi hay gặp, và cách sửa thường là đặt tên kiểu lên trước: Settings::default() thay vì Default::default(). Trang này trình bày các cách sửa trước, rồi giải thích sự khác nhau giữa trait và kiểu, để lỗi này không còn làm bạn bất ngờ. 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[E0790]: cannot call associated function on trait without specifying the corresponding `impl` type
--> src/main.rs:8:20
|
8 | let settings = Default::default(); // ❌ the default of which type?
| ^^^^^^^^^^^^^^^^^^ cannot call associated function of trait
|
help: use a fully-qualified path to a specific available implementation
|
8 | let settings = </* self type */ as Default>::default(); // ❌ the default of which type?
| +++++++++++++++++++ +

Trình biên dịch báo không thể gọi hàm liên kết trên trait khi chưa nói rõ kiểu impl tương ứng, và gợi ý viết đường dẫn đầy đủ. Đây là chương trình gây ra lỗi:

#[derive(Debug, Default)]
struct Settings {
volume: u8,
dark_mode: bool,
}
fn main() {
let settings = Default::default(); // ❌ the default of which type?
println!("{:?}", settings);
}

Bạn cũng nhận cùng lỗi này khi gọi hàm của trait do chính bạn viết qua tên trait, như Shape::new(). Ở đây rustc thấy hai kiểu cài đặt Shape, nên nó liệt kê cả hai làm lựa chọn:

error[E0790]: cannot call associated function on trait without specifying the corresponding `impl` type
--> src/main.rs:33:17
|
2 | fn new() -> Self;
| ----------------- `Shape::new` defined here
...
33 | let shape = Shape::new();
| ^^^^^^^^^^^^ cannot call associated function of trait
|
help: use a fully-qualified path to one of the available implementations
|
33 | let shape = <Circle as Shape>::new();
| ++++++++++ +
33 | let shape = <Square as Shape>::new();
| ++++++++++ +

Với một hằng số của trait như Plugin::ID, dòng tiêu đề hơi khác: cannot refer to the associated constant on trait without specifying the corresponding `impl` type. Cùng vấn đề, cùng cách sửa.

Hãy nhìn những gì rustc đánh dấu:

  1. ^^^^ dưới Default::default(): “cannot call associated function of trait” (không thể gọi hàm liên kết của trait). default là một hàm thuộc về trait Default, và bạn đã gọi nó qua tên trait.
  2. Phần help gợi ý </* self type */ as Default>::default(). Phần /* self type */ là chỗ trống để bạn điền. rustc đang hỏi: “bạn muốn default của kiểu nào?”
  3. Trong ví dụ Shape, rustc điền chỗ trống giúp bạn bằng từng kiểu nó tìm thấy (Circle, Square). Nếu chỉ một kiểu cài đặt trait, nó chỉ gợi ý kiểu đó.

Đây là ý chính. Một trait là một lời hứa: “mọi kiểu cài đặt Default đều tạo được một giá trị khởi đầu của chính nó”. Một kiểu là thứ giữ lời hứa: Settings, String, u32 và Vec<T> mỗi cái giữ lời hứa theo cách riêng. Default::default() đòi chính lời hứa đưa ra một giá trị, nhưng một lời hứa không tự tạo ra được gì. Chỉ kiểu mới làm được.

Hãy nghĩ đến tấm biển “có bán mang về”. Nhiều nhà hàng treo nó ở cửa sổ. Nhưng bạn không thể gọi bữa tối từ tấm biển; bạn phải chọn một nhà hàng. Settings::default() chính là chọn nhà hàng.

Hầu hết các phương thức của trait nhận self, như circle.area(). Những phương thức đó không bao giờ gây ra E0790, vì giá trị cho Rust biết kiểu: circle là một Circle, nên nó gọi area của Circle. Bạn có thể đọc thêm về điều này ở Trait.

Một số hàm của trait không có self: các hàm khởi tạo như new() và default(), các hàm chuyển đổi như From::from, và các hằng số của trait như ID. Không có giá trị nào để nhìn vào, Rust chỉ còn hai nơi để biết kiểu: cái tên bạn viết phía trước (Settings::), hoặc kiểu mà kết quả phải có (let s: Settings = ...). Nếu không có cả hai, Rust không thể đoán. Rất nhiều kiểu cài đặt Default, và chọn bừa một kiểu sẽ âm thầm thay đổi hành vi chương trình của bạn.

Điều này giống với interface trong Java hay C#. Bạn không thể gọi new trên một interface; bạn cần một class cài đặt nó. Rust chỉ là báo cho bạn biết ngay lúc biên dịch, với chính xác chỗ cần sửa.

Gần như luôn chọn cách này. Thay tên trait bằng tên kiểu. Đây là cách sửa ngắn nhất, và đọc lên rất tự nhiên: “Settings mặc định”, “một Circle mới”:

#[derive(Debug, Default)]
struct Settings {
volume: u8,
dark_mode: bool,
}
trait Shape {
fn new() -> Self;
fn name() -> String;
}
struct Circle {
radius: f64,
}
impl Shape for Circle {
fn new() -> Self {
Circle { radius: 1.0 }
}
fn name() -> String {
String::from("hình tròn")
}
}
fn main() {
let settings = Settings::default(); // thay cho Default::default()
let shape = Circle::new(); // thay cho Shape::new()
println!("{:?}", settings);
println!("Một {} có bán kính {}", Circle::name(), shape.radius);
}

Kết quả:

Settings { volume: 0, dark_mode: false }
Một hình tròn có bán kính 1
▶ Tự chạy thử

Circle::new() chạy được dù new được định nghĩa trong trait, không phải trong một khối impl Circle. Rust tìm new trên Circle, thấy nó qua impl Shape for Circle, và gọi nó. Trait phải nằm trong phạm vi (scope) (định nghĩa trong cùng file, hoặc được đưa vào bằng use).

Chọn cách này khi bạn thích phong cách Default::default(), hoặc tên kiểu quá dài. Viết kiểu lên biến, và Rust sẽ suy ra từ đó cần gọi default nào:

#[derive(Debug, Default)]
struct Settings {
volume: u8,
dark_mode: bool,
}
fn main() {
let settings: Settings = Default::default();
let small: u8 = 42;
let big: u32 = From::from(small);
println!("{:?}", settings);
println!("{}", big);
}

Kết quả:

Settings { volume: 0, dark_mode: false }
42
▶ Tự chạy thử

Cách này chỉ dùng được khi hàm trả về chính kiểu đó (Self), như default(), new() và from(). Nó không dùng được cho fn name() -> String: biết kết quả là một String chẳng cho Rust biết bạn muốn hình nào. Với những hàm đó, hãy dùng Cách sửa 1 hoặc Cách sửa 3. Xem thêm về chú thích kiểu ở Kiểu dữ liệu.

Cách sửa 3: Cú pháp đầy đủ <Type as Trait>::function()

Phần tiêu đề “Cách sửa 3: Cú pháp đầy đủ <Type as Trait>::function()”

Đây là dạng “nói hết mọi thứ” mà rustc gợi ý. Hãy đọc <Circle as Shape>::new() là “hàm new mà Circle có nhờ cài đặt Shape”. Nó luôn chạy được, nhưng Circle::new() dễ đọc hơn, nên bạn chỉ cần nó khi dạng ngắn bị mơ hồ:

  • kiểu đó cài đặt hai trait cùng có một hàm trùng tên, hoặc
  • kiểu đó có hàm riêng trùng tên, và bạn muốn phiên bản của trait.

Ở đây một Sale có thể được ghi thành một dòng CSV hoặc một bản báo cáo, và cả hai trait đều có header(). Viết Sale::header() sẽ cho ra E0034 (tiếng Anh) “multiple applicable items in scope”, nên bạn phải nói rõ cái nào:

trait Csv {
fn header() -> String;
}
trait Report {
fn header() -> String;
}
struct Sale;
impl Csv for Sale {
fn header() -> String { String::from("date,amount") }
}
impl Report for Sale {
fn header() -> String { String::from("Doanh số hàng tháng") }
}
fn main() {
println!("{}", <Sale as Csv>::header());
println!("{}", <Sale as Report>::header());
}

Kết quả:

date,amount
Doanh số hàng tháng
▶ Tự chạy thử

Bài Trait nâng cao có thêm một ví dụ, với cả các phương thức nhận &self.

Cách sửa 4: Trong code generic, dùng T:: và turbofish

Phần tiêu đề “Cách sửa 4: Trong code generic, dùng T:: và turbofish”

Chọn cách này khi bạn đang viết một hàm generic. Bên trong fn register<T: Plugin>(), kiểu là T, nên hãy gọi T::name() và T::ID, không phải Plugin::name(). Khi gọi hàm, hãy truyền kiểu bằng turbofish ::<Type>, vì không có đối số nào để Rust biết được kiểu:

trait Plugin {
const ID: u32;
fn name() -> String;
}
struct Logger;
struct Backup;
impl Plugin for Logger {
const ID: u32 = 7;
fn name() -> String { String::from("logger") }
}
impl Plugin for Backup {
const ID: u32 = 12;
fn name() -> String { String::from("backup") }
}
fn register<T: Plugin>() -> String {
format!("Plugin số {}: {}", T::ID, T::name())
}
fn main() {
println!("{}", register::<Logger>());
println!("{}", register::<Backup>());
}

Kết quả:

Plugin số 7: logger
Plugin số 12: backup
▶ Tự chạy thử

Nếu bạn bỏ turbofish và viết register(), bạn sẽ nhận E0283: “cannot infer type of the type parameter T”. Ở đây T cũng là kiểu, nên T:: hoạt động y như Circle:: trong Cách sửa 1. Xem thêm về tham số kiểu ở Generic.

Default::default() mà không có gì để dựa vào. let config = Default::default(); lỗi, nhưng cùng lời gọi đó lại chạy được ở bất cứ đâu kiểu đã được biết: trong một field của struct, một đối số hàm, hoặc cú pháp “điền phần còn lại”:

let settings = Settings { volume: 8, ..Default::default() }; // OK: Rust biết đó là Settings

Hãy sửa dòng bị lỗi bằng Settings::default() hoặc let config: Settings = Default::default();. Xem Các trait thông dụng.

Bên trong một hàm generic. Viết tên trait thay vì T:

error[E0790]: cannot call associated function on trait without specifying the corresponding `impl` type
--> src/main.rs:9:29
|
2 | fn name() -> String;
| -------------------- `Shape::name` defined here
...
9 | format!("This is a {}", Shape::name())
| ^^^^^^^^^^^^^ cannot call associated function of trait
|
help: use the fully-qualified path to the only available implementation
|
9 | format!("This is a {}", <Circle as Shape>::name())
| ++++++++++ +

Cẩn thận: rustc gợi ý <Circle as Shape> vì Circle là hình duy nhất cho tới lúc này. Code đó biên dịch được, nhưng khi đó hàm sẽ luôn luôn nói “circle”, dù T là gì. Cách sửa đúng là T::name(), như trong Cách sửa 4.

Hằng số của trait. println!("{}", Plugin::ID); đòi lời hứa đưa ra một con số, trong khi chỉ từng plugin mới có. Cách sửa: Logger::ID, hoặc T::ID trong code generic.

From::from(x) không có kiểu đích. let total = From::from(42u8); cho ra E0790, vì rất nhiều kiểu tạo được từ một u8. Cách sửa: u32::from(42u8), hoặc let total: u32 = 42u8.into();. Xem Chuyển đổi kiểu.

  • E0282: cần chú thích kiểu. Nếu bạn dùng giá trị sau đó, như let c = Shape::new(); rồi c.area(), rustc sẽ báo E0282. Các cách sửa vẫn như vậy.
  • E0283: cần chú thích kiểu khi nhiều kiểu cùng phù hợp, ví dụ Into::into(n) không có kiểu đích, hoặc một hàm generic được gọi mà không có turbofish.
  • E0599: không tìm thấy hàm. Settings::default() cho ra lỗi này nếu Settings không cài đặt Default; hãy thêm #[derive(Default)]. Với let s: Settings = Default::default(); cùng sai lầm đó lại hiện thành E0277.
  • E0424: dùng self trong một hàm không có tham số self. Cũng là chuyện về các hàm thuộc về một kiểu thay vì một giá trị.
  • Tất cả các lỗi trình biên dịch: danh sách đầy đủ các trang lỗi.

Kiểm tra nhanh

1. let opts = Default::default(); cho ra E0790. Options có derive Default. Dòng nào sửa được?

2. Vì sao let s: Settings = Default::default(); biên dịch được, nhưng let n: String = Shape::name(); vẫn cho ra E0790?

3. Bên trong fn show<T: Shape>(), nên gọi hàm name() của trait như thế nào?

Điểm: 0 / 3