A trait must be in scope¶
Level: 201 · working knowledge
One line: A trait's methods do not exist on a value until the trait itself has been imported — the impl being right there in the file is not enough.
use std::io::Write; // <- without this line, the next one does not compile
fn main() {
let mut out = Vec::new();
out.write_all(b"counted\n").unwrap(); // write_all comes from Write, not from Vec
println!("{}", String::from_utf8_lossy(&out)); // counted
}
Delete the use and rustc says E0599: "items from traits can only be used if the trait is in scope". Nothing about Vec changed; the method was never on Vec in the first place.
Why it works this way¶
A method call is resolved against the traits currently in scope, not against every trait in the universe. That is what stops two crates from silently fighting over the name .next(), and it is why the standard library ships a prelude ↗ — Clone, Iterator, Into, ToString and about twenty others are imported into every file for you, which is the reason .to_string() needs no ceremony while .write_all() does.
So the rule has a corollary worth saying out loud: a trait you can see is not a trait you can call. The impl Shout for Dog block can sit ten lines above dog.shout() in the same file and still not be reachable, if Shout came from a module you imported the type from and not the trait. That is the actual shape of the bug in real code — use loud::Dog; when you needed use loud::{Dog, Shout};.
Three ways to spell the same call¶
Once the trait is in scope, all of these reach the same function:
dog.shout() // the dot: what you write
Shout::shout(&dog) // through the trait
<Dog as Shout>::shout(&dog) // fully qualified: type AND trait named
The dot is sugar. Shout::shout(&dog) is what it desugars to, and the third form is the same again with the ambiguity removed by hand — which matters exactly when there is ambiguity to remove.
The case that forces the long spelling¶
A type may have an inherent method and a trait method with the same name. Both are legal, and they are different functions. The dot always picks the inherent one:
| Spelling | Reaches |
|---|---|
fox.shout() |
the inherent method |
Fox::shout(&fox) |
the inherent method |
<Fox as Shout>::shout(&fox) |
the trait's method |
Only the fully-qualified form can name the trait's version at all. This is also the syntax for an associated function with no receiver — <Dog as Shout>::motto() — where there is no value to put a dot after in the first place.
The verified output¶
Verified output of trait_in_scope.rs — regenerated by tools/run_examples.py, never hand-typed.
1. The method call, now that the trait is in scope
dog.shout() = WOOF
2. The same call, spelled out three ways
Shout::shout(&dog) = WOOF
<Dog as Shout>::shout(&dog) = WOOF
Dog::shout(&dog) = WOOF
3. An associated function has no receiver, so the type carries it
<Dog as Shout>::motto() = LOUD BY DEFAULT
<Fox as Shout>::motto() = LOUD ONLY WHEN ASKED
4. When an inherent method shares the name, the dot picks INHERENT
fox.shout() = (the fox's own shout)
Fox::shout(&fox) = (the fox's own shout)
<Fox as Shout>::shout(&fox) = RING-DING-DING
^ only the last spelling can reach the trait's method at all.
See also¶
- What a trait is — the declaration these calls are reaching into
- "No method named …" — the other three reasons
E0599fires, and thehelp:line that says which one you have implblocks — inherent impl vs trait impl, which is the collision above- Making a
String—.to_string()needs no import becauseToStringis in the prelude
Po polsku¶
To jest reguła, dla której nie ma odpowiednika w Javie, Pythonie ani C++: use wprowadza do zasięgu nie tylko nazwę, ale też zdolność wywołania metody. Dopóki std::io::Write nie zostanie zaimportowane, out.write_all(...) nie istnieje — i to nie dlatego, że Vec czegoś nie ma, tylko dlatego, że tej metody na Vec nigdy nie było. Najbliższy znajomy odpowiednik to metody rozszerzające z C# albo funkcje rozszerzające z Kotlina, gdzie using/import przestrzeni nazw też sprawia, że metody „pojawiają się” na cudzym typie. Komunikat, po którym się to rozpoznaje, to E0599 z linijką „items from traits can only be used if the trait is in scope” — warto ją zapamiętać po angielsku, bo w takiej postaci wypisze ją kompilator.
Wynika stąd wniosek, który w praktyce kosztuje najwięcej czasu: cecha (trait), którą widzisz, to nie to samo, co cecha, którą możesz wywołać. Blok impl Shout for Dog może stać dziesięć linijek wyżej w tym samym pliku i nadal pozostać nieosiągalny, jeśli z modułu zaimportowano typ, a nie cechę — czyli use loud::Dog; tam, gdzie trzeba było napisać use loud::{Dog, Shout};. To samo tłumaczy, dlaczego .to_string() działa bez żadnych ceregieli, a .write_all() nie: ToString siedzi w preludium (prelude), które biblioteka standardowa dołącza do każdego pliku, a Write nie. I nie jest to kaprys — rozstrzyganie wywołań tylko wśród cech obecnych w zasięgu jest dokładnie tym, co powstrzymuje dwa crate'y przed cichą walką o nazwę .next().
Reszta strony pokazuje, że kropka jest lukrem składniowym: dog.shout() rozwija się do Shout::shout(&dog), a pełna forma <Dog as Shout>::shout(&dog) nazywa naraz typ i cechę. Zwykle nie ma to znaczenia — poza jednym przypadkiem, który lepiej znać wcześniej niż później. Gdy typ ma metodę własną (inherent) o tej samej nazwie co metoda z cechy, kropka zawsze wybiera własną: fox.shout() i Fox::shout(&fox) trafiają w metodę własną, a do wersji z cechy dociera wyłącznie <Fox as Shout>::shout(&fox). Ta sama składnia obsługuje funkcje powiązane (associated functions), przy których nie ma nawet wartości, po której dałoby się postawić kropkę: <Dog as Shout>::motto().
Szukaj po polsku: cecha musi być w zasięgu · preludium biblioteki standardowej · rust E0599 trait in scope · rust fully qualified syntax · rust inherent method vs trait method