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 clippyand 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
Personstructure 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
personto theprint_personfunction with the&operator. The&must be added to the signature of theprint_personfunction which os taking a&Personas 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_personfunction 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_inputthat reads first- and last-name from input and constructs a newPersonwith 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
}