Start learning free

LESSON 209 OF 414 · FUNCTIONS & CLASSES

Constructors in Rust: The new Associated Function

Lesson Read the full explanation

TL ; DR:

  • A constructor is code that sets up an object's initial state when you create it. Rust has no special constructor syntax: by convention it is an ordinary associated function.


  • In Rust, a constructor is conventionally an associated function named new inside an impl block:

    struct Dog {
        name: String,
        age: i32,
    }
    
    impl Dog {
        fn new(name: &str, age: i32) -> Dog {
            Dog { name: name.to_string(), age }
        }
    }
    

  • Then create objects with arguments: let rex = Dog::new("Rex", 4); — the constructor fills in the members for you.



Full lesson:

In the previous lesson you set member variables one at a time after creating the object:

let mut dog = Dog { name: String::new(), age: 0 };
dog.name = String::from("Rex");
dog.age = 4;

That works, but it's verbose and easy to forget. Constructors let you initialize the members the moment the object is created.


Defining a constructor

A Rust constructor is conventionally an associated function named new inside an impl block:

  • It is defined with fn new(...) — just a regular associated function.

  • It returns Dog — the type being constructed (inside an impl block you may also write Self for it).

fn new(name: &str, age: i32) -> Dog {
    Dog { name: name.to_string(), age }
}

Struct literal initialization

The parameter name is a borrowed &str, but the field owns a String, so name.to_string() makes an owned copy. The parameter age and the field age have the same identifier, so Rust's field init shorthand lets you write age instead of age: age. No this pointer is needed.


Calling the constructor

let rex = Dog::new("Rex", 4);
let luna = Dog::new("Luna", 7);

By the time these lines finish, rex.name is already "Rex", rex.age is 4, and the same for Luna. Note: no heap allocation is needed for stack-allocated structs.


Your task

The Dog struct already has the name and age fields. Add a new associated function that takes a &str name and an i32 age and returns a Dog using struct literal syntax. Then in mainFunction, create two dogs:

let rex = Dog::new("Rex", 4);
let luna = Dog::new("Luna", 7);

Expected output:

Rex is 4
Luna is 7
STEP 1 OF 5
    Ctrl + Enter to check

    RUST · FUNCTIONS & CLASSES

    Constructors in Rust, explained

    What a constructor is for

    You can create a struct instance by writing every field out, and then change fields one at a time:

    let mut dog = Dog { name: String::new(), age: 0 };
    dog.name = String::from("Rex");
    dog.age = 4;
    

    That works, but it is wordy, and a field can easily be left with a placeholder value. A constructor puts the initialisation in one place, so an instance comes out ready to use.

    Rust has no special constructor syntax. By convention, a constructor is an ordinary associated function named new, written in an impl block. It has no self parameter and returns the new instance. Nothing in the language treats new as special and nothing runs automatically: calling Dog::new(...) is just a function call. Everyone follows the convention, so readers know what to expect.

    Writing new

    For a Dog with a name and an age, the constructor takes both values and builds the struct:

    struct Dog {
        name: String,
        age: i32,
    }
    
    impl Dog {
        fn new(name: &str, age: i32) -> Dog {
            Dog {
                name: name.to_string(),
                age,
            }
        }
    }
    

    Three details do the work:

    • The return type. The function returns a Dog. Writing -> Self means the same thing inside an impl Dog block, because Self is an alias for the type being implemented.
    • Field init shorthand. When a variable has the same name as the field, age alone is enough, instead of age: age. There is no this to write, because the struct expression builds a fresh value.
    • The conversion. The parameter name is a &str, a borrowed slice, but the field is a String that owns its text. name.to_string() makes the owned copy. Skipping it, as in Dog { name, age }, is a compile error: expected String, found &str.

    The struct expression is the last thing in the function and has no semicolon, so it is the returned value.

    Calling the constructor

    Because new has no self, it belongs to the type and is called with :::

    let rex = Dog::new("Rex", 4);
    let luna = Dog::new("Luna", 7);
    

    By the time these lines finish, rex.name is already "Rex" and rex.age is 4, and likewise for luna. Writing rex.new(...) on a value doesn't work, as a function without self can't be called with a dot.

    The compiler checks the call against the signature. Dog::new("Rex") is rejected because the function takes 2 arguments and 1 was supplied. Passing a String where a &str is expected is rejected as a type mismatch: the compiler suggests borrowing it with &.

    The lesson's exercise, step by step

    1. Write new. Inside the empty impl Dog block, declare new with the parameters name: &str and age: i32, returning a Dog. Build the result with a struct expression: the field name gets name.to_string() and the field age uses the shorthand.
    2. Create two dogs. In mainFunction, declare rex as Dog::new("Rex", 4) and luna as Dog::new("Luna", 7). The two println! lines already in the starter code then read their fields.

    The solution in Rust

    struct Dog {
        name: String,
        age: i32,
    }
    
    impl Dog {
        fn new(name: &str, age: i32) -> Dog {
            Dog {
                name: name.to_string(),
                age,
            }
        }
    }
    
    fn mainFunction() {
        let rex = Dog::new("Rex", 4);
        let luna = Dog::new("Luna", 7);
        println!("{} is {}", rex.name, rex.age);
        println!("{} is {}", luna.name, luna.age);
    }
    

    Running mainFunction prints:

    Rex is 4
    Luna is 7
    

    Some further points:

    • Why &str as the parameter. Callers can pass a string literal such as "Rex" directly. A &String also works, because Rust converts it to &str, so a caller who still needs its own String afterwards can lend it with &name.
    • Every field must be set. A struct expression that leaves out a field, like Dog { name: ... } with no age, fails with "missing field age". The constructor is therefore the one place that guarantees complete instances.
    • No overloading. A type can't have two functions both named new with different parameters. Give extra constructors different names instead, for example fn puppy(name: &str) -> Self { Self::new(name, 0) }, which is called as Dog::puppy("Milo").
    • Private fields. When a struct lives in another module and its fields are private, a public new is the only way outside code can build it. That is a common reason to write one.

    Common mistakes

    • Forgetting to_string(). The field needs an owned String.
    • Adding a semicolon after the struct expression. The function then returns nothing and fails to compile.
    • Using self in new. There is no instance yet, so new has no self parameter.
    • Calling it with a dot. Use Dog::new(...), not rex.new(...).
    • Swapping the arguments. Dog::new(4, "Rex") is a type error, because the first parameter is a &str.

    Key takeaways

    • A Rust constructor is an associated function, conventionally named new, that returns Self or the type's name.
    • It has no self parameter and is called with Type::new(...).
    • Field init shorthand lets age stand for age: age.
    • Convert borrowed strings with to_string() when a field owns a String.
    • For several ways to build a value, write several functions with different names.
    Keep going with RustAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free