Back to the basics - Data structures
Until now, we've used the read_string()
function to read a single data item. In this chapter, we'll explore more
complex data structures.
There are many cases where you need to work on multiple data elements that somehow belong together. Think if an address, that consists of: the street, house number, postal code and city. Also, names are typically split in a first name and last name.
In Rust this type of data can be grouped in a struct
. Like so:
struct Address {
street: String,
house_number: i16,
postal_code: String,
city: String
}
Like variables, field names are defined in snake_case. The name of the structure is defined in PascalCase. This means that the name is written without spaces, using capital letters to separate words.
We'll extend our example to ask for both the first- and the last-name.
struct Person { first_name: String, last_name: String, } fn main() { println!("What is your first name?"); let first_name = read_string(); println!("What is your last name?"); let last_name = read_string(); let person = Person { first_name: first_name, last_name: last_name, }; print_person(&person); } fn read_string() -> String { let mut input = String::new(); std::io::stdin() .read_line(&mut input) .expect("can not read user input"); let cleaned_input = input.trim().to_string(); cleaned_input } fn print_person(person: &Person) { println!("Hello {} {}", person.first_name, person.last_name); }
Exercise
Run
cargo clippy
and see if there are any recommendations for this code. Implement the recommendations until Clippy is happy.
Although it is not strictly needed, it is a convention to write data definitions, like the
Person
structure at the top of your code.
We can access individual fields of a struct
with the dot .
separator, as you can see in the print_person
function.
Note that we lend our
person
to theprint_person
function with the&
operator. The&
must be added to the signature of theprint_person
function which os taking a&Person
as input, as well as the calling function that is passing&person
.
Exercise
Add an "age" field to your person and ask the user for their age. Change the
print_person
function to write "You are {} years old." after the greeting.
Adding an associated function
Remember that when we looked at the String
type there was an associated function new()
, that we used to construct
a new String
. We can do the same for type Person
type.
Rewrite the example like this:
struct Person { first_name: String, last_name: String, } impl Person { fn new(first_name: String, last_name: String) -> Person { Person { first_name, last_name, } } } fn main() { println!("What is your first name?"); let first_name = read_string(); println!("What is your last name?"); let last_name = read_string(); let person = Person::new(first_name, last_name); print_person(&person); } fn read_string() -> String { let mut input = String::new(); std::io::stdin() .read_line(&mut input) .expect("can not read user input"); let cleaned_input = input.trim().to_string(); cleaned_input } fn print_person(person: &Person) { println!("Hello {} {}", person.first_name, person.last_name); }
The impl Person { }
block is used to define associated functions, or methods that belong to the Person
type. We've
added a new()
function that takes two input parameters: first_name
and last_name
and returns a new Person
object.
Adding a method
You can add methods in the same impl
block. The only difference between the associated function and a method is in the
type signature. A method always takes &self
as the first input parameter. We can re-write the example to change
print_person
into a print
method on the Person
type:
struct Person { first_name: String, last_name: String, } impl Person { fn new(first_name: String, last_name: String) -> Person { Person { first_name, last_name, } } fn print(&self) { println!("Hello {} {}", self.first_name, self.last_name); } } fn main() { println!("What is your first name?"); let first_name = read_string(); println!("What is your last name?"); let last_name = read_string(); let person = Person::new(first_name, last_name); person.print(); } fn read_string() -> String { let mut input = String::new(); std::io::stdin() .read_line(&mut input) .expect("can not read user input"); let cleaned_input = input.trim().to_string(); cleaned_input }
Note the use of self.first_name
and self.last_name
in the print
method to access the fields of the Person
.
We've also changed print_person(&person)
from the previous example to person.print()
.
Exercise
Try to add an associated function
new_from_input
that reads first- and last-name from input and constructs a newPerson
with this data. All the needed instructions can be found in the above example, within themain()
andPerson::new()
functions.
A Rust struct
can embed other structures. This can be useful when you want to add an address to the Person
. Like so:
struct Address {
street: String,
house_number: i16,
postal_code: String,
city: String
}
struct Person {
first_name: String,
last_name: String,
address: Address
}