Rust needs your help in order to provide the memory safety guarantees. Remember that it is a language with smart pointers (references included) and no garbage collection. Therefore the programmer must provide enough information for the compiler to easily calculate the value lifetimes and check that the code doesn’t lead to dangling pointers accessing values that have been moved or destroyed.
You have no dangling references and no double-free issues in safe code in Rust. You can still have memory leaks but there are techniques to avoid them.
Object construction
Let’s start with simple struct value construction.
struct City {
name: String,
population: i32,
area: i32, // km^2
}
fn main() {
let prague = City {
name: "Prague".to_string(),
population: 1300000,
area: 496,
};
}
Once you move the struct definition to another module, the code no longer works unless you make all members public.
// This would usually be in a separate module.
mod map {
pub struct City {
pub name: String,
pub population: i32,
pub area: i32,
}
}
use map::City;
fn main() {
let prague = City {
name: "Prague".to_string(),
population: 1300000,
area: 496,
};
}}
What if we don’t know the the details about some of the cities?
mod map {
pub struct City {
pub name: String,
pub population: Option<i32>,
pub area: Option<i32>,
}
}
use map::City;
fn main() {
let pilsen = City {
name: "Pilsen".to_string(),
population: None,
area: None,
};
}
You see how our code depends on the struct member list? We want some abstraction.
mod map {
pub struct City {
name: String,
pub population: Option<i32>,
pub area: Option<i32>,
}
impl City {
pub fn new(name: String) -> City {
City {
name: name,
population: None,
area: None,
}
}
}
}
use map::City;
fn main() {
let mut pilsen = City::new("Pilsen".to_string());
pilsen.population = Some(175_000);
}
Now our code doesn’t need to know about the existence of area
but what if
we need a bit more flexibility.
The builder pattern
Let’s say we want to define object construction in terms of an extensible set of function calls but we don’t want to support field modification in a finished object.
mod map {
pub struct City {
name: String,
population: Option<i32>,
area: Option<i32>,
}
impl City {
pub fn name(&self) -> &str { &self.name }
pub fn population(&self) -> Option<i32> { self.population }
}
pub struct CityBuilder {
name: String,
population: Option<i32>,
area: Option<i32>,
}
impl CityBuilder {
pub fn new(name: String) -> CityBuilder {
CityBuilder {
name,
population: None,
area: None,
}
}
pub fn set_population(mut self, population: i32) -> CityBuilder {
self.population = Some(population);
self
}
pub fn build(self) -> City {
City {
name: self.name,
population: self.population,
area: self.area,
}
}
}
}
use map::*;
fn main() {
let pilsen = CityBuilder::new("Pilsen".to_string())
.set_population(175_000)
.build();
if let Some(population) = pilsen.population() {
println!("population of {}: {}", pilsen.name(), population);
}
}
Nobody says the builer data must look the same as the final object data. What if you want to build a more complex object?
mod map {
pub struct Person {
id: i32,
}
pub struct City {
name: String,
people: Vec<Person>,
}
impl City {
pub fn name(&self) -> &str { &self.name }
pub fn population(&self) -> usize { self.people.len() }
}
pub struct CityBuilder {
name: String,
population: i32,
}
impl CityBuilder {
pub fn new(name: &str) -> CityBuilder {
CityBuilder {
name: name.to_string(),
population: 0,
}
}
pub fn set_population(mut self, population: i32) -> CityBuilder {
self.population = population;
self
}
pub fn build(self) -> City {
let people = (0..self.population).map(|x| Person { id: x }).collect();
City {
name: self.name,
people,
}
}
}
}
use map::*;
fn main() {
let pilsen = CityBuilder::new("Pilsen")
.set_population(175_000)
.build();
println!("population of {}: {}", pilsen.name(), pilsen.population());
}
This is enough to provide a simple multi-stage object creation tool. Check out the docs to see how to make a non-consuming builder that can be easily used to create multiple instances.
[https://doc.rust-lang.org/1.0.0/style/ownership/builders.html]
Ownership and moving
Let us create another simple example just to show ownership relation between objects. A country owns its cities that can be borrowed as a slice when you need to access them. New cities are moved into the country.
mod map {
pub struct Country {
name: String,
cities: Vec<City>,
}
impl Country {
pub fn new(name: String) -> Country {
Country {
name,
cities: Vec::new(),
}
}
pub fn add_city(&mut self, city: City) {
self.cities.push(city);
}
pub fn cities(&mut self) -> &[City] {
&self.cities
}
}
#[derive(Debug)]
pub struct City {
name: String,
}
impl City {
pub fn new(name: String) -> City {
City { name }
}
}
}
use map::*;
fn main() {
let mut cz = Country::new("Czechia".to_string());
cz.add_city(City::new("Pilsen".to_string()));
for city in cz.cities() {
println!("{:?}", city);
}
}
Copying and cloning
When you cannot or no not want to transfer ownership, you can make new copies of objects. Let’s slightly adapt a chess example from a previous lesson.
mod chess {
#[derive(Debug, Clone, Copy)]
enum Piece {
King,
Queen,
Bishop,
Knight,
Rook,
Pawn,
}
#[derive(Debug, Clone, Copy)]
enum Color {
White,
Black,
}
#[derive(Debug, Clone, Copy)]
struct Item {
color: Color,
piece: Piece,
}
#[derive(Debug, Clone)]
pub struct Board {
fields: [[Option<Item>; 8]; 8],
current: Color,
}
impl Board {
pub fn new() -> Board {
Board {
fields: [[None; 8]; 8],
current: Color::White,
}
}
}
}
use chess::*;
fn main() {
let board = Board::new();
let another_board = board.clone();
println!("{:?}", board);
println!("{:?}", another_board);
}
Boxed values
You can use Box to allocate space for your whole object (not just String/Vec data) on the heap.
fn main() {
let board = Box::new(Board::new());
let another_board = board.clone();
println!("{:?}", board);
println!("{:?}", another_board);
}
At first look this is a minor optimization to avoid actually moving around filed data when moving the boxed chess board. But the real value of boxed objects will be shown in the next lesson.
References and lifetimes
It is much more difficult to store references than owned objects as you need to take care of object lifetimes.
mod map {
#[derive(Debug)]
pub struct City {
name: String,
}
impl City {
pub fn new(name: String) -> City {
City { name }
}
}
#[derive(Debug)]
pub struct Road<'a> {
src: &'a City,
dst: &'a City,
}
impl Road<'_> {
pub fn new<'a>(src: &'a City, dst: &'a City) -> Road<'a> {
Road { src, dst }
}
pub fn src(&self) -> &City { self.src }
pub fn dst(&self) -> &City { self.dst }
}
}
use map::*;
fn main() {
let prague = City::new("Prague".to_string());
let pilsen = City::new("Pilsen".to_string());
let road1 = Road::new(&prague, &pilsen);
let road2 = Road::new(&pilsen, &prague);
println!("{:?}", road1);
println!("{:?}", road2);
}