Start learning free

LESSON 205 OF 414 · FUNCTIONS & CLASSES

Classes in Rust: Structs, impl Blocks and Methods

Lesson Read the full explanation

Rust has no classes, but it supports object-oriented style: data lives in structs, and methods are attached to them in impl blocks.

Struct creates a user-defined data structure, which holds its own data members (attributes) and member functions (methods), which can be accessed and used by creating an instance of that struct.

Structs are like a blueprint for objects outlining possible behaviors and states that every object of a certain type could have.

Structs are defined via the keyword struct, like this:

// Define an empty struct Employee
struct Employee {
}

Objects/Instances

An Object is an instance of a Struct. A struct is like a blueprint while an instance is a copy of the struct with actual values. It's not an idea anymore, it's an actual employee, like a person named "Andrew" who is 30 years old.

You can have many employees to create many different instances, but without the struct as a guide, you would be lost, not knowing what information is required.

After a struct is defined, we can create objects of the struct. There are two ways of creating objects: locally and on the heap.

Local objects

Local objects behave the same as local variables. They only exist within the scope where they were created. We create a local object just like declaring a variable:

// Define an empty struct Employee
struct Employee {
}

fn main() {
    let employee = Employee {};
}

Heap objects

Heap objects are stored on the heap instead of directly in the variable. Because the data lives on the heap, a Box can be moved out of the scope where it was created, for example returned from a function, and the object stays alive until its final owner goes out of scope.

A heap object is created using the Box::new() function that returns a smart pointer to the newly constructed object:

// Define an empty struct Employee
struct Employee {
}

fn main() {
    let employee = Box::new(Employee {});
}

Notice the Box::new(). This is because employee is not an Employee value directly, but a smart pointer to the created object. We use this pointer to access and manipulate the actual object, just like we use a remote controller for a TV.

Attributes/Properties

A struct attribute is a Rust field that belongs to a struct: every object of this struct has its own copy of it. Let's add some attributes to our Employee struct:

struct Employee {
    pub name: String,
    pub age: i32,
}

fn main() {
    let mut emp1 = Employee { name: String::new(), age: 0 }; // local object

    emp1.name = String::from("Andrew");
    println!("{}", emp1.name); // prints 'Andrew'

    let mut emp2 = Box::new(Employee { name: String::new(), age: 0 }); // heap object

    emp2.age = 40;
    println!("{}", emp2.age); // prints 40
}

We added two properties: the String name and the i32 age. Then, we created two different Employee objects emp1 and emp2.

The attributes of local objects can be accessed and modified using the dot operator (.), like we did above with emp1.name.

The attributes of heap objects can be accessed and modified through the Box pointer using the dot operator (.) as well, since Rust automatically dereferences smart pointers, like we did above with emp2.age.

The keyword pub used before a field name is called an access modifier. name and age are public fields, which means they can be accessed from anywhere in the code, not just inside the struct code.

Constructors

In real life, we want to set the attributes specific to each object when creating it. For example, an employee named Andrew of 30 years old and another one named Mary of 25 years old. Here the constructor comes to rescue.

A constructor is a special type of method (function) which is used to initialize the instance members of the struct.

In Rust the constructor is typically a function named new defined in an impl block and you call it yourself, like Employee::new(...), when you create an object (Rust has no special constructor that runs automatically).

Let's add a constructor to our Employee struct:

struct Employee {
    pub name: String,
    pub age: i32,
}

impl Employee {
    // constructor:
    fn new(name: String, age: i32) -> Employee {
        Employee { name, age }
    }
}

fn main() {
    let emp1 = Employee::new(String::from("Andrew"), 30); // emp1.name = "Andrew", emp1.age = 30
    println!("{}", emp1.name);

    let emp2 = Box::new(Employee::new(String::from("Mary"), 25)); // emp2.name = "Mary", emp2.age = 25
    println!("{}", emp2.age);
}

The "self"

Every method in Rust has access to its own object through an important reference called self.

The self parameter is explicitly listed in all member functions. Therefore, inside a member function, self may be used to access and manipulate the attributes of that object, like we did above with self.name.

Member functions

Member functions are functions that belong to the struct:

struct Employee {
    pub name: String,
    pub age: i32,
}

impl Employee {
    // constructor:
    fn new(name: String, age: i32) -> Employee {
        Employee { name, age }
    }

    //member functions:
    fn print_name(&self) {
        println!("{}", self.name); // use self to access a field
    }

    fn print_employee(&self) {
        self.print_name();
        println!("{}", self.age);
    }
}

fn main() {
    let emp1 = Employee::new(String::from("Andrew"), 30);

    emp1.print_employee(); // prints "Andrew", 30

    let emp2 = Box::new(Employee::new(String::from("Mary"), 25));

    emp2.print_employee(); // prints "Mary", 25
}

As you can see, we access functions just like we access attributes: using the dot syntax (.) for both local and heap objects in Rust, since Box automatically dereferences.

The function call uses the attributes of the object it was called on. That's why the first print_employee() prints 'Andrew' and 30 and the second prints 'Mary' and 25.

Struct

Struct is the primary way to define custom data types in Rust.

In Rust, struct fields are private by default — you need to add pub to make them accessible from outside the module.

struct Employee {
    name: String,
    age: i32,

    // constructor:
    // (defined in impl block)
}

impl Employee {
    fn new(name: String, age: i32) -> Employee {
        Employee { name, age }
    }

    //member functions:
    fn print_name(&self) {
        println!("{}", self.name);
    }

    fn print_employee(&self) {
        self.print_name();
        println!("{}", self.age);
    }
}

fn main() {
    let emp1 = Employee::new(String::from("Andrew"), 30);

    emp1.print_employee();

    let emp2 = Box::new(Employee::new(String::from("Mary"), 25));

    emp2.print_employee();
}


Assignment
Follow the Coding Tutorial and let's write some classes.


Hint
Look at the examples above if you get stuck.


STEP 1 OF 8
    Ctrl + Enter to check

    RUST · FUNCTIONS & CLASSES

    Classes in Rust, explained

    Rust has structs instead of classes

    In many languages a class bundles data with the functions that work on it. Rust has no class keyword, but it gives you the same tools in two separate pieces:

    • a struct declares the data: a named type with named fields;
    • an impl block declares the functions that belong to that type.

    Keeping them apart is deliberate. The struct says what a value contains, and each impl block adds behaviour to it. A value built from a struct is called an instance or an object, and each instance has its own copy of the fields.

    Defining a struct and creating instances

    A struct lists its fields as name: Type, separated by commas:

    struct Job {
        title: String,
    }
    

    An instance is created with a struct expression that gives every field a value, and a field is read or written with a dot:

    let mut job = Job { title: String::from("Designer") };
    println!("{}", job.title); // Designer
    job.title = String::from("Tester");
    println!("{}", job.title); // Tester
    

    Two rules show up right away. Every field must be set when the instance is created: leaving one out is a compile error ("missing field"). And changing a field needs a mut binding: assigning to a field of a plain let variable fails with "cannot assign to job.title, as job is not declared as mutable".

    Methods and associated functions

    An impl Job block holds everything that belongs to the type. It can contain two kinds of functions, and the difference is the first parameter:

    • A function with no self is an associated function. It is called on the type with ::, as in Job::new("Analyst"). By convention, a function named new acts as the constructor.
    • A function whose first parameter is &self is a method. It is called on a value with a dot, as in job.hire_me(). &self borrows the instance, so the method can read its fields without taking ownership. A method that has to change a field takes &mut self instead.

    Inside a method, fields are always reached through self: write self.title, never just title.

    The lesson's exercise

    The exercise builds a Job type from scratch, in eight steps:

    1. Declare the struct Job.
    2. Give it one field, title, of type String.
    3. Open an impl Job block below the struct.
    4. Inside it, declare the associated function new. It takes title: &str, a borrowed string slice, and returns a Job.
    5. In its body, write a Job struct expression whose title field is title.to_string(). The struct owns its text, so the borrowed slice has to be converted into an owned String. Leave off the semicolon: the last expression of a function body is its return value.
    6. Below new, declare the method hire_me with the parameter &self.
    7. In it, print "You're hired as a " followed by self.title, with a {} placeholder in the println! format string.
    8. Write mainFunction: bind job to Job::new("Software Engineer"), then call job.hire_me().

    The solution in Rust

    struct Job {
        title: String,
    }
    
    impl Job {
        fn new(title: &str) -> Job {
            Job { title: title.to_string() }
        }
        
        fn hire_me(&self) {
            println!("You're hired as a {}", self.title);
        }
    }
    
    fn mainFunction() {
        let job = Job::new("Software Engineer");
        job.hire_me();
    }
    

    Running mainFunction prints:

    You're hired as a Software Engineer
    

    Details worth knowing:

    • &str in, String out. The parameter is &str so callers can pass a literal like "Software Engineer" directly. The field is String because the struct must own its data. Putting a literal straight into the field, as in Job { title: "x" }, fails with "expected String, found &str".
    • The semicolon changes the meaning. Job { title: title.to_string() } without a semicolon is returned from new. With a semicolon, the function would return nothing and fail to compile.
    • No self, no dot. new has no self parameter, so Job::new(...) is the only way to call it. A function that uses self.title but forgets to declare &self is rejected: the compiler says self is only available in methods with a self parameter.
    • Each instance is separate. Two jobs made with Job::new("Analyst") and Job::new("Tester") each print their own title when hire_me is called on them.
    • The function name. Rust style puts function names in snake_case, so a compiler warning says mainFunction should be main_function. The program still compiles and runs.

    Heap objects with Box

    An instance created with a struct expression lives in the variable that holds it. If you want it on the heap, wrap it in a Box:

    let boxed = Box::new(Job::new("Analyst"));
    boxed.hire_me();
    println!("{}", boxed.title);
    

    A Box is a smart pointer, and the dot operator sees through it, so fields and methods work exactly as before. Rust frees the boxed value automatically when the Box goes out of scope, unless the box is returned or moved somewhere else first. You rarely need one for a simple struct.

    Privacy

    Struct fields are private by default. Privacy is checked per module: code in the same module as the struct can read the fields freely, as the exercise does, but code in another module can't. To expose a field to other modules you write pub before it, and methods that other modules call need pub fn. Reading a private field from outside fails with "field title of struct Job is private".

    Key takeaways

    • Rust has no classes: a struct holds the data and an impl block holds the functions.
    • Functions without self are called on the type with ::; methods with &self are called on a value with a dot.
    • By convention, new is the constructor and it returns the struct expression as its last line, with no semicolon.
    • A struct usually stores owned data, so convert a &str argument with to_string().
    • Changing fields needs mut, and a method that changes them takes &mut self.
    Keep going with RustAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free