Scalar

Integer

LengthSignedUnsigned
8-biti8u8
16-biti16u16
32-biti32u32
64-biti64u64
128-biti128u128
archisizeusize
isize and usize types depend on the architecture of the computer your program is running on, which is denoted in the table as “arch”: 64 bits if you’re on a 64-bit architecture and 32 bits if you’re on a 32-bit architecture.

Number literals can be written as:

Number literalsExample
Decimal98_222
Hex0xff
Octal0o77
Binary0b1111_0000
Byte (u8 only)b'A'

What happens when integer overflows, in debug mode it panics, in release mode Rust will perform two’s complement wrapping - the value will “wrap around” from maximum to the minimum value, e.g. for u8 the value 256 becomes 0, 257 - 1, and so on.

To explicitly handle the possibility of overflow, you can use these families of methods provided by the standard library for primitive numeric types:

  • Wrap in all modes with the wrapping_* methods, such as wrapping_add.
  • Return the None value if there is overflow with the checked_* methods.
  • Return the value and a boolean indicating whether there was overflow with the overflowing_* methods.
  • Saturate at the value’s minimum or maximum values with the saturating_* methods.

Float

Rust has f32 and f64 types. The default is f64 (on modern CPU the speed is roughly the same as f32 but it’s capable of more precision).

All are signed types.

fn main() {
    let x = 2.0; // f64
    let y: f32 = 3.0; // f32
}

Boolean

Has true and false.

One byte in size.

fn main() {
    let t = true;
    let f: bool = false; // with explicit type annotation
}

Character char

fn main() {
    let c = 'z';
    let z: char = 'ℤ'; // with explicit type annotation
    let heart_eyed_cat = '😻';
}

4 bytes in size.

Represents a Unicode Scalar Value, which means it’s not limited to ASCII.

Compound

Tuple

Have fixed length: once declared cannot grow/shrink in size.

fn main() {
    let tup = (500, 6.4, 1);
	// can be destructured
    let (x, y, z) = tup;
    // or accesed with using a period and index
    let five_hundred = x.0;
    let six_point_four = x.1;
    let one = x.2;
}

The tuple without any values has a special name, unit. This value and its corresponding type are both written () and represent an empty value or an empty return type. Expressions implicitly return the unit value if they don’t return any other value.

Array

fn main() {
    let a = [1, 2, 3, 4, 5];
    // same as
    let a: [i32; 5] = [1, 2, 3, 4, 5];
    
	let first = a[0];
}

All elements must have the same type.

Has fixed length. Can’t grow in size.

Can be initialized with the same initial value like this:

let a = [3; 5]; // [3, 3, 3, 3, 3]

Slice

Struct

Not sure

String

Collections

Unlike the built-in array and tuple types, the data these collections point to is stored on the heap, which means the amount of data does not need to be known at compile time and can grow or shrink as the program runs.

Vector

https://doc.rust-lang.org/stable/std/vec/struct.Vec.html 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.

let v: Vec<i32> = Vec::new();

Rust provides the vec! macro for convenience, which will create a new vector:

let v = vec![1, 2, 3];

To add new elements to the vector, use push() method.

let mut v = Vec::new();
v.push(5);

As with any variable, if we want to be able to change its value, we need to make vector mutable using the mut keyword.

To read elements of vector we either can use indexing syntax of get() method:

let v = vec![1, 2, 3, 4, 5];
let third: &i32 = &v[2];
let third: Option<&i32> = v.get(2);
match third {
	Some(third) => println!("The third element is {third}"),
	None => println!("There is no third element."),
}

Indexing syntax will panic if index outside of vector range.

You can’t have immutable and mutable references to a vector at the same time, including references to its elements, for example this code will not compile:

let mut v = vec![1, 2, 3, 4, 5];
 
let first = &v[0];
 
v.push(6);
 
println!("The first element is: {first}");
$ cargo run
   Compiling collections v0.1.0 (file:///projects/collections)
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
 --> src/main.rs:6:5
  |
4 |     let first = &v[0];
  |                  - immutable borrow occurs here
5 |
6 |     v.push(6);
  |     ^^^^^^^^^ mutable borrow occurs here
7 |
8 |     println!("The first element is: {first}");
  |                                     ------- immutable borrow later used here
 
For more information about this error, try `rustc --explain E0502`.
error: could not compile `collections` (bin "collections") due to 1 previous error

This error is due to the way vectors work: because vectors put the values next to each other in memory, adding a new element onto the end of the vector might require allocating new memory and copying the old elements to the new space, if there isn’t enough room to put all the elements next to each other where the vector is currently stored. In that case, the reference to the first element would be pointing to deallocated memory. The borrowing rules prevent programs from ending up in that situation.

Iteration:

// Immutable references
let v = vec![100, 32, 57];
for i in &v {
	println!("{i}");
}
 
// Mutable references
let mut v = vec![100, 32, 57];
for i in &mut v {
	*i += 50;
}

To change the value that the mutable reference refers to, we have to use the * dereference operator to get to the value in i before we can use the += operator.

To store values of different types we could use enum variants:

enum SpreadsheetCell {
	Int(i32),
	Float(f64),
	Text(String),
}
 
let row = vec![
	SpreadsheetCell::Int(3),
	SpreadsheetCell::Text(String::from("blue")),
	SpreadsheetCell::Float(10.12),
];

If you don’t know the exhaustive set of types a program will get at runtime to store in a vector, the enum technique won’t work. Instead, you can use a trait object.

String

Strings are complicated. Rust has chosen to make the correct handling of String data the default behavior for all Rust programs, which means programmers have to put more thought into handling UTF-8 data upfront. This trade-off exposes more of the complexity of strings than is apparent in other programming languages, but it prevents you from having to handle errors involving non-ASCII characters later in your development life cycle.

Implemented as a collection of bytes.

Rust has only one string type in the core language, which is the string slice str that is usually seen in its borrowed form &str.

The String type, which is provided by Rust’s standard library rather than coded into the core language, is a growable, mutable, owned, UTF-8 encoded string type.

let mut s = String::new();
let s = String::from("initial contents");

All types that implement Display trait have .to_string() method.

let data = "initial contents";
let s = data.to_string();
// the method also works on a literal directly:
let s = "initial contents".to_string();

Strings are UTF-8 encoded, so we can include any properly encoded data in them

let hello = String::from("السلام عليكم");
let hello = String::from("Dobrý den");
let hello = String::from("Hello");
let hello = String::from("שָׁלוֹם");
let hello = String::from("नमस्ते");

We can grow a String by using the push_str method to append a string slice:

let mut s = String::from("foo");
s.push_str("bar");

The push method takes a single character as a parameter and adds it to the String:

let mut s = String::from("lo");
s.push('l');

Combine strings:

let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // note s1 has been moved here and can no longer be used

Looks not that useful to me because the first variable is moved. Better way is to use format! macro:

let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{s1}-{s2}-{s3}");

Compiler can coerce the &String argument into a &str automatically.

Rust strings don’t support indexing. This is because how Rust stores strings in memory.

A String is a wrapper over a Vec<u8>. Unicode scalar value can take more than 1 byte, therefore an index into the string’s bytes will not always correlate to a valid Unicode scalar value.

There are three relevant ways to look at strings from Rust’s perspective: as bytes, scalar values, and grapheme clusters (the closest thing to what we would call letters). For example: “नमस्ते” in vector’s u8 values would look like this:

[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
224, 165, 135]

In Unicode scalar values (Rust’s char):

['न', 'म', 'स', '्', 'त', 'े']

In letters:

["न", "म", "स्", "ते"]

You should use ranges to create string slices with caution, because doing so can crash your program.

let hello = "Здравствуйте";
 
let s = &hello[0..4];

If we were to try to slice only part of a character’s bytes with something like &hello[0..1], Rust would panic at runtime in the same way as if an invalid index were accessed in a vector:

$ cargo run
   Compiling collections v0.1.0 (file:///projects/collections)
    Finished dev [unoptimized + debuginfo] target(s) in 0.43s
     Running `target/debug/collections`
thread 'main' panicked at src/main.rs:4:19:
byte index 1 is not a char boundary; it is inside 'З' (bytes 0..2) of `Здравствуйте`
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

So, how to iterate over strings: 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}");
}
// Output:
// З
// д
 
for b in "Зд".bytes() {
    println!("{b}");
}
// Output:
// 208
// 151
// 208
// 180

But be sure to remember that valid Unicode scalar values may be made up of more than 1 byte.

Getting grapheme clusters from strings as with the Devanagari script is complex, so this functionality is not provided by the standard library.

Hash map

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.

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);
 
for (key, value) in &scores {
	println!("{key}: {value}");
}

Note that we need to first use the HashMap from the collections portion of the standard library. Of our three common collections, this one is the least often used, so it’s not included in the features brought into scope automatically in the prelude. Hash maps also have less support from the standard library; there’s no built-in macro to construct them, for example.

Just like vectors, hash maps store their data on the heap.

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!

If we insert references to values into the hash map, the values won’t be moved into the hash map. The values that the references point to must be valid for at least as long as the hash map is valid.

Overwrite the value:

scores.insert(String::from("Blue"), 10);

Add if key isn’t present:

scores.entry(String::from("Blue")).or_insert(50);

Update the value based on the old value:

use std::collections::HashMap;
 
let text = "hello world wonderful world";
 
let mut map = HashMap::new();
 
for word in text.split_whitespace() {
	let count = map.entry(word).or_insert(0);
	*count += 1;
}
 
println!("{:?}", map);

The or_insert method returns a mutable reference (&mut V) to the value for the specified key. Here we store that mutable reference in the count variable, so in order to assign to that value, we must first dereference count using the asterisk (*). The mutable reference goes out of scope at the end of the for loop, so all of these changes are safe and allowed by the borrowing rules.

By default, HashMap uses a hashing function called SipHash that can provide resistance to Denial of Service (DoS) attacks involving hash tables. You can switch to another function by specifying a different hasher. A hasher is a type that implements the BuildHasher trait.