Vectors
Vectors allow you to store more than one value in a single data structure that puts all the values next to each other in memory. Vectors can only store values of the same type. They are similar to vector in C++.
Create a vector
let v: Vec<i32> = Vec::new();
Here we had to define the type cause we haven’t put any value in the vector.
Generally rust compiler will infer the type of data that is stored inside the vector if put an element while defining the vector.
let v = vec![1, 2, 3];
Update a vector
We use the push method to push element to the back of the vector
let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
v.push(8);
Reading elements of a vector
In rust we have two way to get the value of a vector at an index - []
,get()
. The vectors in rust at 0 indexed so first element is at index 0.
let v = vec![1, 2, 3, 4, 5];
let third: &i32 = &v[2];
println!("The third element is {third}");
let third: Option<&i32> = v.get(2);
match third {
Some(third) => println!("The third element is {third}"),
None => println!("There is no third element."),
}
Iterating over the values in a vector
To iterate over elements in a vector we can use for loop to get a mutable or immutable reference of elements in a vector
let v = vec![1, 2, 3, 4, 5];
for i in &b {
println!("{i}")
}
let mut v = vec![100, 32, 57];
for i in t &mut v {
*i += 50;
}
Vectors (Vec<T>
) implement the iter()
method, which returns an iterator over references to the elements in the vector.
let v = vec![1, 2, 3, 4, 5];
for i in v.iter() {
println!("{i}")
}
Using an enum to store multiple types
Vectors can only store values that are of the same type. This can be inconvenient; there are definitely use cases for needing to store a list of items of different types
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
Dropping a vector
Vector in rust implement the Drop
trait i.e. when a vector goes out of scope it would be freed.
{
let v = vec![1, 2, 3, 4];
// do stuff with v
} // <- v goes out of scope and is freed here
String
Rust only has one string type in the core language, which is str
- string literal.
The String
type, which is provided by rust’s standard library is a growable, mutable, owned, UTF-8, heap allocated string type.
A String
is a wrapper around Vec<u8>
but the len()
method return the number of bytes not the number of characters. For example the string "hola"
is 4 bytes long(each char is 1-byte in UTF-8) but the string "Здравствуйте"
is 24 bytes long(each cyrillic character is 2 bytes long in UTF-8).
Since String
is UTF-8 encoded this means that a character can take 1 to 4 bytes to store a char making byte level indexing unreliable. There if we do &s[0]
this would give us a compilation error.
Creating a new string
Many of the same operations available with Vec<T>
are available with String
as well because String
is actually implemented as a wrapper around a vector of bytes with some extra guarantees, restrictions, and capabilities
let mut s = String::new();
let v = String::new("hello")
This creates a new, empty string s
into which we can load data. v
is a string that is allocated to with initialized string.
Updating a string
We can grow a string using push_str
method to append a string slice.
let mut s = String::from("foo");
s.push_str("bar");
this appends the string literal "bar"
to the string "foo"
. The push_str
doesn’t take ownership of the string literal.
The push
method can be used to append a single character.
let mut s = String::from("lo");
s.push('l');
Concatenation of string
In Rust, the +
operator is used to concatenate strings
Using the + Operator:
- The
+
operator uses theadd
method, which has the signaturefn add(self, s: &str) -> String
. - When you use
+
, the first string (s1
) is moved into theadd
method, meaning it is no longer valid after the operation. - The second string (
s2
) is passed as a reference (&s2
), and Rust automatically coerces&String
to&str
using deref coercion. - The result is a new String that combines the contents of
s1
ands2
.
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // s1 is moved, s2 remains valid
If we need to concatenate multiple strings, the behavior of the + operator gets unwieldy:
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = s1 + "-" + &s2 + "-" + &s3;
this can be made easier using the format!
macro.
let s = format!("{s1}-{s2}-{s3}"); // all are valid afterwards
Ways to interpret string
Rust provides three perspectives for working with strings:
- Bytes: The raw byte representation (e.g., [224, 164, 168, …] for the Hindi word “नमस्ते”).
- Scalar Values (Unicode): The char type, which represents Unicode scalar values (e.g., [‘न’, ‘म’, ‘स’, ‘्’, ‘त’, ‘े’] for “नमस्ते”).
- Grapheme Clusters: The closest thing to human-readable “letters” (e.g., [“न”, “म”, “स्”, “ते”] for “नमस्ते”). Each perspective is useful depending on the context, and Rust allows you to choose the appropriate one.
Iterating over a string
The best way to operate on pieces of strings is to be explicit about whether you want characters or bytes.
for c in "Зд".chars() {
println!("{c}");
}
// З
// д
for b in "Зд".bytes() {
println!("{b}");
}
// 208
// 151
// 208
// 180
Hash Maps
The type HashMap<K, V>
stores a mapping of keys of type K
to values of type V
using a hashing function, which determines how it places these keys and values into memory
Creating a new map
One way to create an empty hash map is to use new
and to add elements with insert
.
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
Accessing elements in a hash map
We can get a value out of the hash map by providing its key to the get
method.
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
let team_name = String::from("Blue");
let score = scores.get(&team_name).copied().unwrap_or(0);
Iterating over a hash map
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
for (key, value) in &scores {
println!("{key}: {value}");
}
Ownership in hash map
For types that implement the Copy trait, like i32
, the values are copied into the hash map. For owned values like String
, the values will be moved and the hash map will be the owner of those values,
use std::collections::HashMap;
let field_name = String::from("Favorite color");
let field_value = String::from("Blue");
let mut map = HashMap::new();
map.insert(field_name, field_value);
// field_name and field_value are invalid at this point, try using them and
// see what compiler error you get!
Overwriting a value
If we insert a key and a value into a hash map and then insert that same key with a different value, the value associated with that key will be replaced.
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Blue"), 25);
println!("{scores:?}");
Adding a Key and Value Only If a Key Isn’t Present
Hash maps have a special API for this called entry that takes the key you want to check as a parameter. The return value of the entry method is an enum called Entry that represents a value that might or might not exist.
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.entry(String::from("Yellow")).or_insert(50);
scores.entry(String::from("Blue")).or_insert(50);
println!("{scores:?}");