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
- Named fields struct example:
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,
}
}