TODO: recap early sessions in the training
This document lists my personal notes for further reference taken during my Rust training on Ûdemy (Link)
TODO complete this section
- debug formatting:
println!("{my_value:?}")
- stored on the stack
- implement copy trait, therefore the following creates two different values on the stack
let a: i32 = 5;
let mut b: i32 = a; // copies the original, we have now two different variables stored on the stack
b += 5;
println!("{a} {b}");-
String - A dynamic piece of text stored on the heap at runtime
-
str - A hardcoded, read-only piece of text encoded in the binary
-
do not mistake
&str -
uses the
Stringnamespace -
is stored on the heap
-
only a reference (a pointer) is stored in the stack
-
allocates some extra memory that can be used during string manipulation
let one: String = String::new();
let mut two: String = String::from("Two");
two.push_str(" Three");
println!("{two}");- a variable is more appropriately called a binding in Rust
- The binding linked to a value is responsible for the clean up of the value upon leaving its scope
- There can be only one owner at a time
- the
dropfunction is called upon cleaning of variable, i.e. when it goes out of scope. This only works for heap allocated variables - the
clonemethod allows deep copy of object when allocated on the heap - you cannot take ownership to a value stored in another structure like an array or a tuple as it expects to remain owner of it. For example, the following is not allowed:
fn main() {
let my_array = [String::from("one"), String::from("two"), String::from("three")];
let a = my_array[0]; // not allowed; either borrow a reference or make a copy with clone()
}- transfer of ownership from one owner to another
- transfer happens if when copying variable that doesn't implement the copy trait. For example a String stored on the heap:
let person = String::from("Ben");
let employee = person; // the ownership has been transfered to the employee variable - have a reference to a value without ownership
- use the borrow operator
&
fn main() {
let my_stack_value: i32 = 2;
let my_int_ref: &i32 = &my_stack_value; // note the data type
let heap_value: String = String::from("horse");
let heap_reference: &String = &heap_value;
}- reference like these are guaranteed to always be valid (References never outlive their referent)
- de-reference operator:
*(accesses the data at the reference)
let heap_reference: &String = &heap_value;
println!("{}", *heap_reference);- Rust ensures that references implement the display traits by using the display trait of the actual value behind the reference
- The immutable references (not the mutable to avoid breaking the rule of single mutable reference to a value at the same time) implement the copy trait
let ref1: &str = "This is a test";
let ref2: &str = ref1; // we have now two different references on the stack pointing to the same encoded string in the binary
println!("{ref1} {ref2}");- a mutable reference is automatically dereferenced when useed:
let mut value = String::from("test");
let ref = &mut value;
ref.push_str(" extension");- to borrow a mutable reference, the original variablee must also be mutable
- there can be multiple immutable reference to a same value at the same time
- However there can only be one single mutable reference to a value at a time. After a mutable reference has been created, it is not permitted to create even an immutable reference anymore
- small caveat: the compiler is smart enough to detect if the mutable reference not used after some point and thereefore allows the creation of an immutable ref
- a dangling reference is a reference to a value that has been cleared/deallocated. This is forbideen by the compiler. For example, the following is not allowed:
fn main() {}
fn create_city() -> &String {
let city = String::from("Berlin");
return &city;
}- by default, parameters are immutables. To change this:
fn main() {
let my_string = String::from("This is a test");
print_my_string(my_string);
}
fn print_my_string(mut value: String) {
value.push_str(" [String extension]");
println!("Your value is: {value}");
}- depending on the datatype, a function parameter will use a copy of the variable passed (if it implements the copy trait) or will move ownership
fn main() {
let my_value: i32 = 155;
print_my_value(my_value); // in this case the value is copied
let my_string = String::from("This is a test");
print_my_string(my_string); // ownership moved from the variable to the parameter.
println!("my_string is not valid anymore: {my_string}");
}
fn print_my_value(value: i32) {
println!("Your value is {value}");
}
fn print_my_string(value: String) {
println!("Your value is {value}");
}- To only borrow a reference, change parameter declaration like this:
fn show_meal(meal: &String) { // fn show_meal(meal: &mut String) {...} if you want to modify it along the way
println!("Meal: {meal}");
}- moves ownership from the function local variable to the caller via the return (implicit or explicit)
fn main() {
let cake = bake_cake();
println!("I have a cake: {cake}");
}
fn bake_cake() -> String {
// let cake = String::from("Chocolate mousse");
// return cake;
String::from("Chocolate mousse")
}- a slice is a reference to a portion/sequence of a collection (e.g. array) up to 100% of the collection
fn main() {
let name: String = String::from("Hyojj Xipitug");
let firstname: &str = &name[0..5]; // From index 0 included up to index 5 NOT included
println!("{firstname}");
}- slice boundaries can be skipped if we want to start fro, the beginning and/or finish at the end
fn main() {
let action_hero = String::from("Arnold Schwarzenegger");
let first_name = &action_hero[..6];
println!("His first name is {first_name}.");
let last_name = &action_hero[7..];
println!("His last name is {last_name}.");
let full_name = &action_hero[..];
println!("His full name is {full_name}.");
}- deref coercion is the mechanism that Rust uses to deref an argument as long as possible until it reaches a valid type. This makes the following code valid:
fn do_hero_stuff(hero_name: &str) { // &String -> String -> &str
println!("{hero_name} saves the day!");
}
fn main() {
let action_hero = String::from("Arnold Schwarzenegger");
do_hero_stuff(&action_hero); //deref coercion
let another_hero = "Sylvester Stallone";
do_hero_stuff(another_hero);
}- It is therefore more flexible to have
&stras argument type - same logic of slicing with arrays:
fn main() {
let values: [i32; 6] = [4, 8, 15, 16, 23, 42];
let my_slice: &[i32] = &values[0..3]; // note that the type doesn't mention the length here
println!("{my_slice:?}");
}- using slice allows more flexibility than the stricter reference:
fn main() {
let values: [i32; 6] = [4, 8, 15, 16, 23, 42];
let my_slice: &[i32] = &values[..]; // compare this type... (array slice)
println!("{my_slice:?}");
let my_slice: &[i32; 6] = &values; // ...with this type (reference to a full array, length included)
println!("{my_slice:?}");
}- Rust does not allow mutable slice of string but well of arrays
- Class == Struct in Rust
- 3 types of struct:
- Named fields structs
- Tuple-like structs
- Unit-like structs
fn main() {
struct CoffeeDrink {
price: f64,
name: String,
is_hot: bool,
}
let mocha = CoffeeDrink {
name: String::from("Mocha"),
price: 4.99,
is_hot: true,
};
println!("{}", mocha.name);
}- from an ownership point of view, the struct instance is the owner of its fields which are in turn owners of their respective values
[...]
let favorite_coffee = mocha.name; // /!\ this takes ownership of the inner field: movement of ownership
println!("{}", mocha.name); // will fail because not owner anymore- when declared mutable, a struct instance's fields are all mutable
- Shorthand syntax to instantiate struct:
fn make_coffee(name: String, price: f64, is_hot: bool) -> CoffeeDrink {
CoffeeDrink {
price,
name,
is_hot,
}
}- Struct update syntax: shortcut to populate part of the fields from another instance of the same struct (concerned fields must implement copy trait; reason explained below). The fields that are mentioned before the .. syntax are not overriden
struct CoffeeDrink {
price: f64,
name: String,
is_hot: bool,
}
fn main() {
let beverage = make_coffee(String::from("Latte"), 4.99, true);
println!("{}", beverage.name);
let caramel_machiato = CoffeeDrink {
name: String::from("Caramel Machiato"),
..beverage // <= Magic happens here...
};
println!("{}", caramel_machiato.name);
}
fn make_coffee(name: String, price: f64, is_hot: bool) -> CoffeeDrink {
CoffeeDrink {
price,
name,
is_hot,
}
}- the struct update can lead to ownership issue. If one the field does not implement the copy trait, it will lead to an ownership move instead. If need be, use
clone()method - Here is how to derive (implement a default version) the debug trait (Trait is akin to interface in other languages):
#[derive(Debug)]
struct CoffeeDrink {
price: f64,
name: String,
is_hot: bool,
}
fn main() {
let beverage =
CoffeeDrink {
price: 4.99,
name: String::from("Latte"),
is_hot: true,
};
println!("{:#?}", beverage);
}- Method implementation (
⚠️ the self parameter can be a struct value or a reference; the later one is more flexible although the first one can be useful as well). Methods are implemented in a separated block from the datastructure declaration. There can be any arbitrary number of implementation blocks
#[derive(Debug)]
struct Song {
title: String,
release_year: u32,
duration_sec: u32,
}
impl Song {
fn display_song_info(&self) { // equivalent to self: &Self where Self is the current struct we are implementing
println!("Song title is {}", self.title);
}
}
fn main() {
let song = Song{
duration_sec: 100,
release_year: 2026,
title: String::from("Pif paf pouf"),
};
song.display_song_info();
}- A bit more convoluted example:
#[derive(Debug)]
struct Song {
title: String,
release_year: u32,
duration_sec: u32,
}
impl Song {
fn display_song_info(&mut self) { // the type 'Self' is a shortcut to mention the struct we are implementing
println!("Song title is {}, released in {} and its duration is {}", self.title, self.release_year, self.duration_sec);
}
fn increase_duration(&mut self, extra_duration: u32) {
self.duration_sec += extra_duration;
}
fn is_longer_than(&self, other: &Self) -> bool {
self.duration_sec > other.duration_sec
}
}
fn main() {
let mut song = Song{
duration_sec: 100,
release_year: 2026,
title: String::from("Pif paf pouf"),
};
song.display_song_info();
song.increase_duration(15);
song.display_song_info();
let another = Song {
duration_sec: 200,
release_year: 2026,
title: String::from("Wouapdouwap"),
};
println!("song longer than another? {}", song.is_longer_than(&another));
}- Associated functions are functions associated to a type (equivalent in other languages: static methods): simply do not mention self in the parameter list. They are invoked with :: syntax
impl Song {
[...]
fn new(title: String, release_year: u32, duration_sec: u32) -> Self {
Self {
title,
release_year,
duration_sec,
}
}
}
fn main() {
let song = Song::new(String::from("Pif paf pouf"), 2026, 100);
song.display_song_info();
}- builder pattern. Allows methods chaining in an elegant manner
#[derive(Debug)]
struct Computer {
cpu: String,
memory: u32,
hard_drive_capacity: u32,
}
impl Computer {
fn new(cpu: String, memory: u32, hard_drive_capacity: u32,) -> Self {
Self {
cpu,
memory,
hard_drive_capacity,
}
}
fn upgrade_cpu(&mut self, cpu: String) -> &mut Self{
self.cpu = cpu;
self
}
fn upgrade_memory(&mut self, memory: u32) -> &mut Self{
self.memory = memory;
self
}
fn upgrade_hard_drive_capacity(&mut self, hard_drive_capacity: u32) -> &mut Self{
self.hard_drive_capacity = hard_drive_capacity;
self
}
}
fn main() {
let mut computer = Computer::new(String::from("M3"), 64, 1);
computer
.upgrade_cpu(String::from("M4 Pro"))
.upgrade_memory(128)
.upgrade_hard_drive_capacity(2);
println!("{computer:#?}");
}- fields are not named, only their positions matter
struct ShortDuration(u32, u32);
impl ShortDuration {
fn diplay_info(&self) {
println!("Workshift duration is {} hours, {} minutes", self.0, self.1);
}
}
fn main() {
let workshift = ShortDuration(7, 36);
workshift.diplay_info();
}- Unit is an empty tuple
- not often used, usually in some edge case design patterns. You may derive the debug trait as for structs
struct Empty;
fn main() {
let my_empty = Empty;
}- not much to say, very comparable to equivalent in other languages
#[derive(Debug)]
enum CardSuit {
Heart,
Club,
Diamond,
Spade
}
fn main() {
let first_card = CardSuit::Club;
println!("The card's suit is {first_card:?}");
}- enum can be associated to value like so.
#[derive(Debug)]
enum PaymentType {
CreditCard(String),
DebitCard,
PayPal,
}
fn main() {
let visa = PaymentType::CreditCard(String::from("My account number here"));
}- there can be more than one associated value from different types
- It is not mandatory for all variants to store the same data/datatype
- associated data can alternatively be provided as a struct
#[derive(Debug)]
enum PaymentType {
CreditCard(String),
DebitCard,
PayPal { username: String, password: String },
}
fn main() {
let paypal = PaymentType::PayPal {
password: String::from("password"),
username: String::from("username"),
};
println!("{paypal:?}");
}- enum used with the
matchkeyword:
enum OS {
Windows,
MacOS,
Linux
}
fn main() {
let computer = OS::MacOS;
println!("MacOS was created {} years ago", years_since_release(&computer));
}
fn years_since_release (os: &OS) -> u32 {
match os {
OS::Windows => {
println!("Some complex logic here...");
39 // no semi-colon ; so it is considered an implicit return
}
OS::MacOS => 23,
OS::Linux => 34,
}
}- it is possible to access the associated values of an enum in match structure like so:
enum LaundryCycle {
Cold,
Hot {temperature: u32},
Delicate (String)
}
fn main() {
let cold = LaundryCycle::Cold;
wash_laundry(&cold);
let silk = LaundryCycle::Delicate(String::from("silk"));
wash_laundry(&silk);
}
fn wash_laundry(cycle: &LaundryCycle) {
match cycle {
LaundryCycle::Cold => {
println!("Lavage à froid")
}
LaundryCycle::Hot {temperature} => {
println!("Lavage à {temperature}°C")
}
LaundryCycle::Delicate (fabric_type) => {
println!("Taking extra care of {fabric_type} fabric");
}
}
}- functions can be attached to an enum the same way it would to a struct. This leads to the following refactored code:
enum LaundryCycle {
Cold,
Hot {temperature: u32},
Delicate (String)
}
impl LaundryCycle {
fn wash_laundry(&self) {
match self {
LaundryCycle::Cold => {
println!("Lavage à froid")
}
LaundryCycle::Hot {temperature} => {
println!("Lavage à {temperature}°C")
}
LaundryCycle::Delicate (fabric_type) => {
println!("Taking extra care of {fabric_type} fabric");
}
}
}
}
fn main() {
LaundryCycle::Cold.wash_laundry();
LaundryCycle::Delicate(String::from("silk")).wash_laundry();
LaundryCycle::Hot { temperature: 80 }.wash_laundry();
}matchniceness
#[derive(Debug)]
enum OnlineOrderStatus {
Ordered,
Packed,
Shipped,
Delivered
}
impl OnlineOrderStatus {
fn check(&self) {
match self {
OnlineOrderStatus::Ordered | OnlineOrderStatus::Packed => {
println!("Still on our premises");
}
OnlineOrderStatus::Delivered => {
println!("Your order has been delivered");
}
other => {
println!("Your order has NOT been delivered: ({other:?})");
}
}
}
}
fn main() {
OnlineOrderStatus::Ordered.check();
OnlineOrderStatus::Delivered.check();
OnlineOrderStatus::Shipped.check();
}- match arms can be very specific in the way they match their values:
#[derive(Debug)]
enum Milk {
LowFat(i32),
WholeMilk,
}
impl Milk {
// just for sport, we take ownership of the value this time
// the value will therefore be cleaned at the end of the method
fn drink(self) {
match self {
// will only match LowFat with exactly 2 associated value
Milk::LowFat(2) => {
println!("Very specific match arm");
}
Milk::LowFat(percent) => {
// must come as second arm, otherwise it will always take precedence over the very specific arm
println!("Drinking unspecific low fat milk ({percent}%)");
}
Milk::WholeMilk => {
println!("Drinking whole milk");
}
}
}
}
fn main() {
Milk::WholeMilk.drink();
Milk::LowFat(2).drink();
Milk::LowFat(10).drink();
}- enums and if-let construction
#[derive(Debug)]
enum Milk {
LowFat(i32),
Whole,
NonDairy {kind: String}
}
fn main() {
let beverage = Milk::Whole;
// if let <static hard-coded> = <dynamic value to compare against>
// this way of doing allows to only verify one specific enum possibility
// opposite to the match statement that checks everything
if let Milk::Whole = beverage {
println!("You have wole milk");
}
}- Associated value can also be retrieved:
[...]
if let Milk::LowFat(percent) = beverage {
println!("Fat percentage : {percent}% ");
}- let-else construct
#[derive(Debug)]
enum Milk {
LowFat(i32),
Whole,
NonDairy {kind: String}
}
fn main() {
let beverage = Milk::Whole;
let Milk::LowFat(percent) = beverage else {
// this block will only be executed if beverage is not LowFat
return; // mandatory for the compiler, otherwise we could try to use percent later even if not defined
};
println!("{percent} retrieved in the let-else construct");
}Optionmodels situation where we can either have a valid value or none at all. Compare it to the Null value in other languages
fn main() {
let a = Option::Some(5); // auto-detect the generic type based on the literal
let b: Option<i8> = Option::Some(5); // one way to override the generic type
let c = Option::<i8>::Some(5); // another way
let d: Option<i32> = Option::None; // the None variant forces us to provide the generic type as it cannot infer it from the code
}- actual usage example
fn main() {
let instruments = [
String::from("Guitar"),
String::from("Piano"),
String::from("Bass"),
];
// safe way to get an element without the risk of an out of bound exception
// note that the generic type is Option<&String> to avoid taking array element ownership
let bass: Option<&String> = instruments.get(2);
println!("{bass:?}");
let far_away: Option<&String> = instruments.get(200);
println!("{far_away:?}");
}- a generic is a type argument == placeholder for future type
- Behind the scene, the compiler generate all functions variants needed to compile. Therefore the types that can be handled must be known at compile time
fn identity<T> (value: T) -> T {
value
}- Turbofish operator:
::<i32>, used to enforce the type to use with the generic
fn main() {
let value = identity::<f64>(5);
println!("{value}");
}
fn identity<T> (value: T) -> T {
value
}- Generics work the same with structs:
struct TreasureChest<T> {
captain: String,
treasure: T,
}
impl TreasureChest<String> {
// provide implementation specifically for the String generic variant
}
impl<T> TreasureChest<T> { // we need to mention the impl<T> otherwise the compiler would only try to resolve T as a type for a specific type (see above to understand the ambiguity)
// provide an implementation for any type
}- Generic can also be used with enums
enum Cheesesteak<T> {
Plain,
Topping(T)
}