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.
Thông báo lỗi
Phần tiêu đề “Thông báo lỗi”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.
Ý nghĩa
Phần tiêu đề “Ý nghĩa”Hãy nhìn những gì rustc đánh dấu:
^^^^dướiDefault::default(): “cannot call associated function of trait” (không thể gọi hàm liên kết của trait).defaultlà một hàm thuộc về traitDefault, và bạn đã gọi nó qua tên trait.- 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ốndefaultcủa kiểu nào?” - 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.
Vì sao Rust làm vậy
Phần tiêu đề “Vì sao Rust làm vậy”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.
Cách sửa
Phần tiêu đề “Cách sửa”Cách sửa 1: Gọi nó trên kiểu
Phần tiêu đề “Cách sửa 1: Gọi nó trên kiểu”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 1Circle::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).
Cách sửa 2: Chú thích kiểu cho biến
Phần tiêu đề “Cách sửa 2: Chú thích kiểu cho biến”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 }42Cá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,amountDoanh số hàng thángBà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: loggerPlugin số 12: backupNế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.
Thường gặp ở đâu
Phần tiêu đề “Thường gặp ở đâu”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à SettingsHã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.
Lỗi liên quan
Phần tiêu đề “Lỗi liên quan”- E0282: cần chú thích kiểu. Nếu bạn dùng giá trị sau đó, như
let c = Shape::new();rồic.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ếuSettingskhông cài đặtDefault; hãy thêm#[derive(Default)]. Vớilet s: Settings = Default::default();cùng sai lầm đó lại hiện thành E0277. - E0424: dùng
selftrong 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
Phần tiêu đề “Kiểm tra nhanh”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
Đọc thêm
Phần tiêu đề “Đọc thêm”- E0790 — Rust error codes index (tiếng Anh): giải thích chính thức về lỗi này, kèm một ví dụ ngắn và cách sửa.
- Traits — The Rust Book (tiếng Anh): trait là gì, cách cài đặt một trait, và cài đặt mặc định.
- Advanced Traits — The Rust Book (tiếng Anh): có phần về cú pháp đầy đủ, với ví dụ kinh điển
<Dog as Animal>::baby_name(). - Disambiguating overlapping traits — Rust by Example (tiếng Anh): một ví dụ ngắn chạy được về việc chọn giữa hai trait cùng có một phương thức trùng tên.
Default— tài liệu thư viện chuẩn (tiếng Anh): deriveDefault, tự cài đặt nó, và mẫu..Default::default().