What an abstract class does, and Rust's version
An abstract class is a template that is not complete enough to use directly. It declares some methods without a body, and every concrete subtype must supply them. Two rules make it useful: you can't create an instance of the abstract type itself, and a subtype that forgets a required method is rejected.
Rust has no classes, so it has no abstract classes. A trait plays the same role:
- A method declared in a trait without a body is required. Every type that implements the trait must define it.
- A method declared with a body is a default that implementors may keep or replace.
- A trait is not a type you can build a value of. Only structs, enums and other concrete types have values.
What a trait cannot do is hold fields. If implementors need shared data, each struct declares its own fields, or the shared parts are composed into it. The trait describes behaviour only.
The two guarantees, checked by the compiler
Take the lesson's trait:
trait Animal {
fn speak(&self);
}
First, a type that claims to implement Animal but leaves out speak does not compile. For impl Animal for Dog {}, rustc reports error[E0046]: not all trait items implemented, missing: `speak`. That is the "contract with teeth" that abstract methods provide.
Second, Animal can't be used as the type of a plain value. A statement such as let b: Animal; fails with error[E0782]: expected a type, found a trait, and let a = Animal; fails because a trait is not a value. You have to choose a concrete type that implements it. (A Box<dyn Animal> can hold any such type, but there is always a concrete value behind it.)
Step by step
- Declare a unit struct,
struct Dog;. A unit struct has no fields, which is all this lesson's dog needs, and its value is written simply asDog. - Open the implementation with
impl Animal for Dog { }. The trait name comes first and the type second: you implement a trait for a type. Trait methods can only be called on types that have such a block. - Write the required method inside the block, with exactly the trait's signature:
fn speak(&self). The body printsWoofwithprintln!.
After those steps, let rex = Dog; rex.speak(); in the lesson's main function calls the method you wrote.
The solution in Rust
trait Animal {
fn speak(&self);
}
struct Dog;
impl Animal for Dog {
fn speak(&self) {
println!("Woof");
}
}
fn mainFunction() {
let rex = Dog;
rex.speak();
}
The output is:
Woof
Mixing required and default methods
An abstract class often has concrete methods that call the abstract ones. A trait does the same with default methods:
trait Animal {
fn name(&self) -> String;
fn speak(&self);
fn intro(&self) {
print!("{} says: ", self.name());
self.speak();
}
}
struct Dog;
impl Animal for Dog {
fn name(&self) -> String { "Rex".to_string() }
fn speak(&self) { println!("Woof"); }
}
fn main() {
Dog.intro();
}
This prints:
Rex says: Woof
intro has a body, so Dog does not write it. It is inherited from the trait in the sense that matters here: it is available on every implementor, and it calls the required methods, which each type defines for itself.
Common mistakes
- Changing the signature. The implementation must match the trait:
fn speak(&self), notfn speak(self)or a different return type. A mismatch is a compile error. - Putting the method in a plain
impl Dogblock. A method written there belongs toDogbut does not satisfy the trait. The method must be insideimpl Animal for Dog. - Calling the method with the trait out of scope. If the trait is defined in another module, it has to be imported with
usebefore its methods can be called on a value. - Adding a field to the trait. Traits can't declare data fields. Put shared data in the structs, and expose it through a required method like
name. - Writing a body in the trait declaration and expecting it to be required. Only methods without a body are mandatory.
Key takeaways
- A trait with required methods is Rust's counterpart of an abstract class's abstract methods.
- The compiler rejects an implementation that is missing a required method.
- A trait can't be instantiated; only concrete types have values.
- Default methods give shared behaviour, but shared data lives in the structs.
