André Torres logger.debug("welcome")

Notes on Rust: Collections

Rust has two types of collections:

  • Stored in the stack: Like array and tuples, you need to know the sizes of those collections so Rust can allocate the right amount of memory.
  • The ones stored in the heap: The point of those are to be with dynamic length, so you don’t need to know their size beforehand. The main ones are vector, string and hash map.

Vector

The first one is the vector, which is a dynamic length list, where you can add or remove values. To instantiate a new vector you do:

// When using Vec::new() we need to declare the type
// because it is a generic function and
// the type can't be inferred automatically
let v: Vec<i32> = Vec::new();

// In case you already have an initial value you can use vec!
// which will create a vector with the values passed and it will
// also infer the type for you
let v = vec![1, 2, 3];

Even those dynamic data structures are immutable, so if you want to add items to it you need to declare them as mutable.

// This will throw an error
let immutable_vector: Vec<i32> = Vec::new();

immutable_vector.push(1); // error[E0596]: cannot borrow `immutable_vector` as mutable, as it is not declared as mutable

// This is allowed
let mut mutable_vector: Vec<i32> = Vec::new();

mutable_vector.push(1);
mutable_vector.push(2);
mutable_vector.push(3);

Memory safety

Now, if you are storing something in a vector, you probably want to read it. There are two ways of doing data.

The unsafe way using &vector[index] that gives you a reference of the value:

let v: Vec<i32> = vec![1, 2, 3, 4, 5];
let id = &v[0];
println!("Something {}", id); // Something 1

let invalid_index = &v[10];
println!("Somethin else {}", invalid_index); // thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 10', src/main.rs:3:15

There’s also the safe way with vector.get(index) that will return an Option<&T>.

let v: Vec<i32> = vec![1, 2, 3, 4, 5];
let id = v.get(1);
match id {
   Some(id) => println!("The selected element is {}", id), 
   None => println!("No valid element was selected"),
} // The selected element is 2

let invalid_element = v.get(20);
match invalid_element {
   Some(invalid_element) => println!("The selected element is {}", invalid_element),
   None => println!("No valid element was selected"),
} // No valid element was selected

Now on Vectors and Safety, when you get a value from the value from a vector, you get all the borrow checks and memory ownership safeguard that rust has.

When you have the ownership of a value from the vector you can’t modify the same.

fn main() {
	let mut v: Vec<i32> = vec![1, 2, 3, 4, 5];
  let value = &v[1]; // &v[1] or v.get(1) will enforce the rules
  v.push(6);
  println!("The element at 1 is {}", value);
}

When you try to compile, you will receive:

23 |     let value = &v[1];
   |                  - immutable borrow occurs here
24 |     v.push(6);
   |     ^^^^^^^^^ mutable borrow occurs here
25 |     println!("The element at 1 is {}", value);
   |                                        ----- immutable borrow later used here

Also when a vector is dropped from scope the values are also dropped. The borrow checker will make sure that no one is using a value.

Finally you can easily iterate over vectors too

let v = vec![100, 32, 57];
for i in &v {
	println!("{i}");
}

String

Rust has two types of string types. The &str and the String, the first one is stored in the stack and have fixed size. The other one is a dynamic one, where you can manipulate it. Here I will only talk about the second one.

We create those strings doing the following

let empty_string = String::new()
let test_string = String::from("Test");
let from_string = "from string".to_string();

You can update Strings if they are mutable

let mut s1 = String::from("foo");
let s2 = "bar";
s.push_str("_");
s.push_str(s2);
s.push('!'); // String.push() accepts a character
println!("s1 is {s1}"); // s1 is foo_bar

Syntax sugar with + and format!

You can combine strings using +, but you need to understand how the ownership will affect the variables.

let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2;
println!("s1 is {s1}, s2 is {s2}, Value of s3: {s3}");

This will throw the following error during compilation

error[E0382]: borrow of moved value: `s1`
 --> src\main.rs:5:22
  |
2 |     let s1 = String::from("Hello, ");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 |     let s2 = String::from("world!");
4 |     let s3 = s1 + &s2;
  |              -- value moved here
5 |     println!("s1 is {s1}, s2 is {s2}, Value of s3: {s3}");
  |                      ^^ value borrowed here after move
  |

You can’t have &str + &str because both are immutable. So in case you try to concatenate them

--> src\main.rs:4:18
  |
4 |     let s3 = &s1 + &s2;
  |              --- ^ --- &String
  |              |   |
  |              |   `+` cannot be used to concatenate two `&str` strings
  |              &String
  |
  = note: string concatenation requires an owned `String` on the left
help: remove the borrow to obtain an owned `String`
  |
4 -     let s3 = &s1 + &s2;
4 +     let s3 = s1 + &s2;
  |

So if you use the + the ownership will be passed to the new variable. You can also use format! to concatenate strings, with the advantage of not having to pass the ownership to the new value

fn main() {
    let s1 = String::from("tic");
    let s2 = String::from("tac");
    let s3 = String::from("toe");

    let s = format!("{s1}-{s2}-{s3}");
    println!("Value of s is {s}"); // Value of s is tic-tac-toe
	  println!("s1 is {s1}, s2 is {s2}, s3 is {s3}"); // s1 is tic, s2 is tac, s3 is toe
}

Slicing and Iterating

You can’t use [] to get a single character from a String but you can use to get a slice of a String with &var[0..10].

let text = "a string with some text";
let slice = &text[0..8];
println!("Sliced string is: {slice}"); // Sliced string is a string

You can also iterate other the over the string with .chars() or .bytes()

let text = "a string with some text";
for c in text.chars() {
    print!("{c}");
}
println!("");
for b in text.bytes() {
    println!("{b}");
}

This will output to

a string with some text
9732115116114105110103321191051161043211511110910132116101120116

UTF-8 and why strings are not so simple.

Rust decided to favour safety over abstracting the complexity. Which makes quite different to work with strings in Rust, so I recommend reading

Storing UTF-8 Encoded Text with Strings - The Rust Programming Language

Hashmaps

HashMaps are to store data based on keys. This is how we can use the HashMap:

// You need to import from the collections
use std::collections::HashMap;

fn main() {
    // You need to create a mutable HashMap if you 
    // want to add anything to it
    let mut scores = HashMap::new();

    // Inserting the values to the HashMap
    // The key and value must be the same for all values
    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);

    // Iterating over the HashMap
    for (key, value) in &scores {
        println!("{key}: {value}");
    }
}

You can easily retrieve the value from the HashMap:

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);

		// get - Retrieve the value from the HashMap, this will give you an Option<&V>
    let blue_score = scores.get("Blue");
    match blue_score {
        Some(blue_score) => println!("Blue score is {blue_score}"),
        None => println!("None"),
    }
    println!("Blue score is {blue_score}"); // Blue score is 10
}

There is also some convenient method for common operations

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);

		// Combine entry and or_insert to insert a value if isn't in the HashMap
    scores.entry(String::from("Red")).or_insert(30);
		let red_score = scores.get("Red");
    match red_score {
        Some(red_score) => println!("Red score is {red_score}"),
        None => println!("None"),
    }

		// You can also update the value in the same way
    let green_score = scores.entry(String::from("Green")).or_insert(20);		
    *green_score += 5;
		let green_score = scores.get("Green");		
    match green_score {
        Some(green_score) => println!("Green score is {green_score}"),
        None => println!("None"),
    }
}