Start learning free

LESSON 407 OF 414 · OBJECT-ORIENTED PROGRAMMING

Abstract Classes in Rust: Traits with Required Methods

Lesson Read the full explanation

TL ; DR:

Rust has no abstract classes. Its closest equivalent is a trait: a template that declares methods with no body, which any concrete type that implements the trait must provide. You can't create a value of a trait itself, only of a type that implements it.


trait Animal {
    fn speak(&self);  // abstract — must be implemented by concrete types
}
struct Dog;
impl Animal for Dog {
    fn speak(&self) { println!("Woof"); }
}


Full lesson:

Sometimes you want to capture shared behavior across a family of types without committing to one concrete implementation. For example, every Animal can speak(), but exactly how depends on whether it's a Dog or a Cat. That's what abstract classes are for in other languages, and what traits are for in Rust.

Two superpowers

  1. You can't create a bare Animal value — a trait is not a type you can build. You have to pick a concrete type that implements it.
  2. Any type that implements the trait MUST provide every method that has no default body, or the compiler reports an error. The trait is a contract with teeth.

Mixing abstract and concrete

A trait can also have default methods (with bodies). Those are shared by every implementing type for free. Only the methods declared without a body must be implemented. A trait cannot hold fields; the data lives in the structs.

Notes by language

  • Required method — write the signature followed by a semicolon: fn speak(&self);.
  • Default method — give it a body inside the trait; implementing types may keep it or replace it.
  • Implementing — write impl Animal for Dog { ... }. There is no abstract or override keyword.

Your task

The Animal trait is already defined. Declare a unit struct Dog (struct Dog;) and write impl Animal for Dog with a speak method that prints "Woof". mainFunction builds a Dog and calls speak().

Expected output:

Woof
STEP 1 OF 2
    Ctrl + Enter to check

    RUST · OBJECT-ORIENTED PROGRAMMING

    Abstract Classes in Rust, explained

    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

    1. 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 as Dog.
    2. 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.
    3. Write the required method inside the block, with exactly the trait's signature: fn speak(&self). The body prints Woof with println!.

    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), not fn speak(self) or a different return type. A mismatch is a compile error.
    • Putting the method in a plain impl Dog block. A method written there belongs to Dog but does not satisfy the trait. The method must be inside impl 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 use before 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.
    Keep going with RustAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free