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-> Selfmeans the same thing inside animpl Dogblock, becauseSelfis an alias for the type being implemented. - Field init shorthand. When a variable has the same name as the field,
agealone is enough, instead ofage: age. There is nothisto write, because the struct expression builds a fresh value. - The conversion. The parameter
nameis a&str, a borrowed slice, but the field is aStringthat owns its text.name.to_string()makes the owned copy. Skipping it, as inDog { name, age }, is a compile error: expectedString, 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
- Write
new. Inside the emptyimpl Dogblock, declarenewwith the parametersname: &strandage: i32, returning aDog. Build the result with a struct expression: the fieldnamegetsname.to_string()and the fieldageuses the shorthand. - Create two dogs. In
mainFunction, declarerexasDog::new("Rex", 4)andlunaasDog::new("Luna", 7). The twoprintln!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
&stras the parameter. Callers can pass a string literal such as"Rex"directly. A&Stringalso works, because Rust converts it to&str, so a caller who still needs its ownStringafterwards can lend it with&name. - Every field must be set. A struct expression that leaves out a field, like
Dog { name: ... }with noage, fails with "missing fieldage". The constructor is therefore the one place that guarantees complete instances. - No overloading. A type can't have two functions both named
newwith different parameters. Give extra constructors different names instead, for examplefn puppy(name: &str) -> Self { Self::new(name, 0) }, which is called asDog::puppy("Milo"). - Private fields. When a struct lives in another module and its fields are private, a public
newis 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 ownedString. - Adding a semicolon after the struct expression. The function then returns nothing and fails to compile.
- Using
selfinnew. There is no instance yet, sonewhas noselfparameter. - Calling it with a dot. Use
Dog::new(...), notrex.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 returnsSelfor the type's name. - It has no
selfparameter and is called withType::new(...). - Field init shorthand lets
agestand forage: age. - Convert borrowed strings with
to_string()when a field owns aString. - For several ways to build a value, write several functions with different names.
