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
implblock 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
selfis an associated function. It is called on the type with::, as inJob::new("Analyst"). By convention, a function namednewacts as the constructor. - A function whose first parameter is
&selfis a method. It is called on a value with a dot, as injob.hire_me().&selfborrows the instance, so the method can read its fields without taking ownership. A method that has to change a field takes&mut selfinstead.
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:
- Declare the struct
Job. - Give it one field,
title, of typeString. - Open an
impl Jobblock below the struct. - Inside it, declare the associated function
new. It takestitle: &str, a borrowed string slice, and returns aJob. - In its body, write a
Jobstruct expression whosetitlefield istitle.to_string(). The struct owns its text, so the borrowed slice has to be converted into an ownedString. Leave off the semicolon: the last expression of a function body is its return value. - Below
new, declare the methodhire_mewith the parameter&self. - In it, print
"You're hired as a "followed byself.title, with a{}placeholder in theprintln!format string. - Write
mainFunction: bindjobtoJob::new("Software Engineer"), then calljob.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:
&strin,Stringout. The parameter is&strso callers can pass a literal like"Software Engineer"directly. The field isStringbecause the struct must own its data. Putting a literal straight into the field, as inJob { title: "x" }, fails with "expectedString, found&str".- The semicolon changes the meaning.
Job { title: title.to_string() }without a semicolon is returned fromnew. With a semicolon, the function would return nothing and fail to compile. - No
self, no dot.newhas noselfparameter, soJob::new(...)is the only way to call it. A function that usesself.titlebut forgets to declare&selfis rejected: the compiler saysselfis only available in methods with aselfparameter. - Each instance is separate. Two jobs made with
Job::new("Analyst")andJob::new("Tester")each print their own title whenhire_meis called on them. - The function name. Rust style puts function names in snake_case, so a compiler warning says
mainFunctionshould bemain_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
structholds the data and animplblock holds the functions. - Functions without
selfare called on the type with::; methods with&selfare called on a value with a dot. - By convention,
newis the constructor and it returns the struct expression as its last line, with no semicolon. - A struct usually stores owned data, so convert a
&strargument withto_string(). - Changing fields needs
mut, and a method that changes them takes&mut self.
