Post

Rust Data Structures: Arrays, Vectors, Slices, Strings, Tuples, HashMaps, and More

A concise practical guide to Rust's core data structures, including arrays, vectors, slices, tuples, unit type, strings, string slices, HashMaps, and common collection patterns.

Rust Data Structures: Arrays, Vectors, Slices, Strings, Tuples, HashMaps, and More

Rust Data Structures

Rust has a relatively small set of built-in data structures, plus a rich standard-library collection API.

The most important ones to understand are:

1
2
3
4
5
6
7
8
9
Array       [T; N]
Vector      Vec<T>
Slice       &[T]
Tuple       (T1, T2, ...)
Unit        ()
String      String
Str slice   &str
HashMap     HashMap<K, V>
HashSet     HashSet<T>

A useful first distinction is:

1
2
3
4
5
6
7
8
9
10
11
12
13
Fixed size:
    [T; N]
    (T1, T2, ...)
    
Dynamic size:
    Vec<T>
    String
    HashMap<K, V>
    HashSet<T>

Borrowed views:
    &[T]
    &str

1. Array

An array contains a fixed number of values of the same type.

1
let numbers = [1, 2, 3, 4, 5];

Its type is:

1
[i32; 5]

The 5 is part of the type.

Therefore:

1
2
let a: [i32; 3] = [1, 2, 3];
let b: [i32; 5] = [1, 2, 3, 4, 5];

These are different types:

1
2
[i32; 3]
[i32; 5]

2. Creating Arrays

You can explicitly specify every element:

1
let numbers = [1, 2, 3, 4, 5];

Or repeat the same value:

1
let zeros = [0; 5];

This means:

1
[0, 0, 0, 0, 0]

You can also specify the type:

1
let numbers: [i32; 5] = [1, 2, 3, 4, 5];

3. Accessing Array Elements

Array indexing starts at zero:

1
2
3
4
let numbers = [10, 20, 30];

println!("{}", numbers[0]);
println!("{}", numbers[2]);

Output:

1
2
10
30

An invalid index causes a panic:

1
2
3
let numbers = [10, 20, 30];

println!("{}", numbers[10]);

For safe access, use get():

1
let value = numbers.get(10);

The result is:

1
None

because get() returns:

1
Option<&T>

This is a good example of how Rust’s data structures interact with Option.


4. Arrays Are Stored Inline

An array’s elements are stored directly as part of the array.

Conceptually:

1
2
3
4
5
6
let numbers = [10, 20, 30];

stack:
+----+----+----+
| 10 | 20 | 30 |
+----+----+----+

There is no separate heap allocation required just to store the array.

This makes arrays useful when the number of elements is known at compile time.


5. Array Length

Use:

1
2
3
let numbers = [1, 2, 3, 4];

println!("{}", numbers.len());

Result:

1
4

The length is also part of the type:

1
[i32; 4]

6. Array Iteration

You can iterate over an array:

1
2
3
4
5
let numbers = [1, 2, 3];

for number in numbers {
    println!("{number}");
}

You can also borrow the array:

1
2
3
for number in &numbers {
    println!("{number}");
}

The distinction matters when ownership is involved.


7. Vector: Vec<T>

A vector is Rust’s growable array.

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

Its type is:

1
Vec<i32>

Unlike:

1
[i32; 3]

a vector can grow or shrink at runtime.

1
2
3
4
let mut numbers = vec![1, 2, 3];

numbers.push(4);
numbers.push(5);

Now:

1
[1, 2, 3, 4, 5]

8. Array vs Vector

The basic difference:

1
2
3
4
5
6
7
8
9
10
11
12
13
Array:
    [T; N]

    fixed size
    size is part of the type
    stored inline

Vector:
    Vec<T>

    dynamic size
    heap allocation
    can grow and shrink

For example:

1
2
3
4
let array = [1, 2, 3];

let mut vector = vec![1, 2, 3];
vector.push(4);

9. Creating a Vector

Using the vec! macro:

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

Using Vec::new():

1
2
3
4
5
let mut values = Vec::new();

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

Using Vec::with_capacity():

1
let mut values = Vec::with_capacity(100);

This allocates enough capacity for approximately 100 elements, but the vector’s length is still zero.

1
2
values.len();      // 0
values.capacity(); // at least 100

10. Length vs Capacity

This distinction is important.

1
let mut values = Vec::with_capacity(10);

Initially:

1
2
length   = 0
capacity >= 10

After:

1
values.push(42);

we have:

1
2
length   = 1
capacity >= 10

Think of:

1
2
3
4
5
6
length
    = number of elements currently stored

capacity
    = number of elements that can currently fit
      before another allocation may be required

11. Vector Methods

Common methods include:

1
2
3
4
5
6
7
8
9
10
11
12
13
push()
pop()
insert()
remove()
get()
first()
last()
len()
is_empty()
clear()
contains()
sort()
reverse()

Example:

1
2
3
4
5
6
7
let mut values = vec![1, 2, 3];

values.push(4);

let last = values.pop();

println!("{:?}", last);

Result:

1
Some(4)

Notice that pop() returns:

1
Option<T>

because the vector might be empty.


12. Slice

A slice is a dynamically sized view into a contiguous sequence.

The most common form is:

1
&[T]

For example:

1
2
3
let numbers = [1, 2, 3, 4, 5];

let slice = &numbers[1..4];

The slice contains:

1
2, 3, 4

The important thing is:

A slice does not own the data.

It is a borrowed view.


13. Slice From a Vector

Slices can also borrow a vector:

1
2
3
let numbers = vec![1, 2, 3, 4, 5];

let slice = &numbers[1..4];

Now:

1
2
3
4
5
6
7
Vec
+---+---+---+---+---+
| 1 | 2 | 3 | 4 | 5 |
+---+---+---+---+---+
    ^
    |
    +--- slice [2,3,4]

The vector owns the data.

The slice only borrows it.


14. Why Use Slices?

Suppose we write:

1
2
3
fn sum(values: &[i32]) -> i32 {
    values.iter().sum()
}

Now we can pass both arrays:

1
2
3
let array = [1, 2, 3];

sum(&array);

and vectors:

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

sum(&vector);

This is extremely useful.

Instead of writing separate functions for:

1
2
fn sum_array(...)
fn sum_vector(...)

we can write:

1
fn sum(values: &[i32])

because both arrays and vectors can be viewed as slices.


15. Slice Syntax

The general syntax is:

1
&collection[start..end]

The end index is exclusive.

1
2
3
let values = [0, 1, 2, 3, 4];

let slice = &values[1..4];

gives:

1
1, 2, 3

because:

1
2
start = 1
end   = 4

and index 4 is excluded.


16. Slice Range Syntax

You can omit either side.

From the beginning:

1
&values[..3]

means:

1
0..3

From an index to the end:

1
&values[2..]

means:

1
2..len

The entire collection:

1
&values[..]

means the entire slice.


17. Mutable Slices

You can also borrow a mutable slice:

1
2
3
4
5
fn double(values: &mut [i32]) {
    for value in values {
        *value *= 2;
    }
}

Then:

1
2
3
let mut values = vec![1, 2, 3];

double(&mut values);

Now:

1
[2, 4, 6]

The function doesn’t care whether the original data came from:

1
2
3
Vec
Array
another Slice

It only requires:

1
&mut [i32]

18. str and &str

This is one of the most confusing parts of Rust.

There are two important types:

1
2
String
&str

String owns its string data.

&str is a borrowed string slice.

Conceptually:

1
2
3
4
5
6
7
String
    |
    +-- owns UTF-8 bytes

&str
    |
    +-- borrows UTF-8 bytes

19. String

String is an owned, growable UTF-8 string.

1
2
3
let mut name = String::from("Amir");

name.push_str(" Nikpour");

Now:

1
Amir Nikpour

Because String owns its data, it can grow.


20. Creating a String

Common ways:

1
2
3
4
5
6
7
let s = String::new();

let s = String::from("hello");

let s = "hello".to_string();

let s = format!("Hello, {}", name);

For example:

1
2
3
4
let mut message = String::new();

message.push_str("Hello ");
message.push('A');

21. &str

A string literal:

1
"hello"

has type:

1
&'static str

It is a reference to string data embedded in the program.

For example:

1
let name: &str = "Amir";

The variable does not own the string data.


22. String vs &str

A useful mental model:

1
2
3
4
5
6
7
8
9
10
String
+------------------------+
| pointer                |
| length                 |
| capacity               |
+------------------------+
          |
          v
      heap bytes
      "hello"

While:

1
2
3
4
5
6
7
8
&str
+----------------+
| pointer        |
| length         |
+----------------+
        |
        v
    UTF-8 bytes

&str is essentially a fat pointer containing:

1
pointer + length

It does not contain capacity because it does not own or manage the allocation.


23. Converting Between String and &str

From String to &str:

1
2
3
let name = String::from("Amir");

let slice: &str = &name;

You can also use:

1
let slice = name.as_str();

From &str to String:

1
2
3
let slice = "Amir";

let name = slice.to_string();

or:

1
let name = String::from(slice);

This creates owned data.


24. Function Parameters: Prefer &str When You Only Need to Read

Instead of:

1
2
3
fn greet(name: String) {
    println!("Hello {name}");
}

if the function does not need ownership, use:

1
2
3
fn greet(name: &str) {
    println!("Hello {name}");
}

Now both work:

1
2
3
4
let name = String::from("Amir");

greet(&name);
greet("Amir");

This is an important Rust API-design pattern.

If a function only needs to read a string, &str is often more flexible than String.


25. String Slices

A String can be sliced:

1
2
3
let hello = String::from("Hello world");

let hello_slice = &hello[0..5];

Result:

1
Hello

But remember that Rust strings are UTF-8.


26. Rust Strings Are UTF-8

A String is not an array of characters.

For example:

1
let s = String::from("hello");

contains UTF-8 bytes.

This is why:

1
s[0]

is not allowed.

Instead, you can iterate over characters:

1
2
3
for c in s.chars() {
    println!("{c}");
}

Or bytes:

1
2
3
for b in s.bytes() {
    println!("{b}");
}

These are different concepts:

1
2
3
4
5
bytes
    -> UTF-8 encoded bytes

chars()
    -> Unicode scalar values

27. String Methods

Common methods include:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
push()
push_str()
pop()
len()
is_empty()
clear()
contains()
starts_with()
ends_with()
replace()
replace_range()
split()
chars()
bytes()
as_str()

Example:

1
2
3
4
5
6
let mut s = String::from("hello");

s.push('!');
s.push_str(" Rust");

println!("{s}");

28. Tuple

A tuple groups values of potentially different types.

1
let user = ("Amir", 29, true);

Its type is:

1
(&str, i32, bool)

Unlike arrays:

1
2
3
4
5
Array:
    same type

Tuple:
    different types allowed

29. Accessing Tuple Values

Tuple fields are accessed using indexes:

1
2
3
4
5
let user = ("Amir", 29, true);

println!("{}", user.0);
println!("{}", user.1);
println!("{}", user.2);

Output:

1
2
3
Amir
29
true

Tuple indexes are known at compile time.


30. Tuple Destructuring

You can destructure a tuple:

1
2
3
4
5
6
7
let user = ("Amir", 29, true);

let (name, age, active) = user;

println!("{name}");
println!("{age}");
println!("{active}");

This is called pattern destructuring.

You can ignore values:

1
let (name, _, active) = user;

31. Nested Tuples

Tuples can contain other tuples:

1
let value = (1, ("hello", true));

Type:

1
(i32, (&str, bool))

32. Unit Type ()

The unit type is:

1
()

It represents exactly one value:

1
()

It is used when there is no meaningful value to return.

For example:

1
2
3
fn print_message() {
    println!("hello");
}

is essentially:

1
2
3
fn print_message() -> () {
    println!("hello");
}

33. () Is a Real Type

This is important:

1
()

is not “nothing”.

It is a value of a real type:

1
2
unit type: ()
unit value: ()

For example:

1
let value: () = ();

This is why:

1
Result<(), Error>

means:

1
2
3
4
5
Ok(())
    -> operation succeeded

Err(error)
    -> operation failed

34. Struct

Structs are one of Rust’s main ways of creating custom data structures.

1
2
3
4
struct User {
    name: String,
    age: u32,
}

Create one:

1
2
3
4
let user = User {
    name: String::from("Amir"),
    age: 29,
};

Access fields:

1
2
println!("{}", user.name);
println!("{}", user.age);

35. Tuple Struct

A tuple struct has unnamed fields:

1
struct Point(i32, i32);

Create:

1
let point = Point(10, 20);

Access:

1
2
println!("{}", point.0);
println!("{}", point.1);

Tuple structs are useful when the fields have meaning but don’t need names.


36. Enum

An enum represents one of several possible variants.

1
2
3
4
5
enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
}

Create:

1
let message = Message::Write(String::from("hello"));

Enums are especially powerful in Rust because variants can contain different data.


37. Option Is an Enum

For example:

1
2
3
4
enum Option<T> {
    Some(T),
    None,
}

So:

1
Option<String>

can contain either:

1
Some(String)

or:

1
None

38. Result Is an Enum

Similarly:

1
2
3
4
enum Result<T, E> {
    Ok(T),
    Err(E),
}

This is why Option and Result are closely related to Rust’s general enum system.


39. HashMap

HashMap<K, V> stores key-value pairs.

1
2
3
4
5
6
use std::collections::HashMap;

let mut scores = HashMap::new();

scores.insert("Alice", 10);
scores.insert("Bob", 20);

Conceptually:

1
2
3
4
HashMap

"Alice" -> 10
"Bob"   -> 20

Keys are used to find values.


40. Accessing a HashMap

Use get():

1
let score = scores.get("Alice");

The result is:

1
Option<&i32>

Why?

Because the key might not exist.

1
2
3
4
5
key exists
    -> Some(&value)

key doesn't exist
    -> None

This is another common use of Option.


41. HashMap Indexing

You can also write:

1
let score = scores["Alice"];

But if the key does not exist, indexing can panic.

Therefore:

1
scores.get("Alice")

is safer when the key might be missing.


42. Updating a HashMap

Insert a value:

1
scores.insert("Alice", 10);

If the key already exists:

1
scores.insert("Alice", 20);

the previous value is replaced.

The return value of insert() is:

1
Option<V>

It returns the old value if there was one.


43. entry() API

The entry() API is extremely useful.

Suppose we want to count words:

1
2
3
4
5
6
7
8
use std::collections::HashMap;

let mut counts = HashMap::new();

for word in ["hello", "world", "hello"] {
    let count = counts.entry(word).or_insert(0);
    *count += 1;
}

Result:

1
2
hello -> 2
world -> 1

The important pattern is:

1
map.entry(key).or_insert(default)

If the key exists, it returns a mutable reference to the existing value.

If it doesn’t exist, it inserts the default.


44. or_insert() and or_insert_with()

1
map.entry(key).or_insert(value);

uses an already-created value.

For expensive initialization:

1
map.entry(key).or_insert_with(|| expensive_operation());

The closure is evaluated only when the entry does not exist.

This follows the same eager/lazy pattern seen elsewhere in Rust.


45. HashMap Iteration

Iterate over keys and values:

1
2
3
for (key, value) in &scores {
    println!("{key}: {value}");
}

Keys only:

1
2
3
for key in scores.keys() {
    println!("{key}");
}

Values only:

1
2
3
for value in scores.values() {
    println!("{value}");
}

Mutable values:

1
2
3
for value in scores.values_mut() {
    *value += 1;
}

46. HashSet

A HashSet<T> stores unique values.

1
2
3
4
5
6
7
use std::collections::HashSet;

let mut users = HashSet::new();

users.insert("Alice");
users.insert("Bob");
users.insert("Alice");

The second "Alice" is not added as a duplicate.

Conceptually:

1
2
Alice
Bob

47. HashSet Membership

Use:

1
2
3
if users.contains("Alice") {
    println!("Alice exists");
}

contains() returns:

1
bool

48. HashSet vs HashMap

Think of:

1
2
3
4
5
HashSet<T>
    = collection of unique values

HashMap<K, V>
    = collection of key -> value pairs

For example:

1
2
3
4
5
6
7
8
9
HashSet:
    Alice
    Bob
    Charlie

HashMap:
    Alice -> 10
    Bob   -> 20
    Charlie -> 30

49. VecDeque

For queue-like data structures:

1
2
3
4
5
6
7
use std::collections::VecDeque;

let mut queue = VecDeque::new();

queue.push_back(1);
queue.push_back(2);
queue.push_front(0);

Result:

1
0 1 2

Remove from the front:

1
let value = queue.pop_front();

Remove from the back:

1
let value = queue.pop_back();

VecDeque is useful when you need efficient insertion/removal from both ends.


50. LinkedList

Rust also provides:

1
std::collections::LinkedList<T>

but it is much less commonly useful than Vec or VecDeque.

A linked list stores nodes connected through links:

1
Node -> Node -> Node -> Node

Compared with Vec:

1
2
Vec:
[1][2][3][4]

For most ordinary workloads, a contiguous Vec is often preferable because of cache locality and simpler memory access.

Use LinkedList only when its particular characteristics actually solve a problem.


51. BTreeMap

Rust also provides:

1
use std::collections::BTreeMap;

It stores key-value pairs in an ordered tree structure.

1
2
3
4
let mut users = BTreeMap::new();

users.insert("Alice", 10);
users.insert("Bob", 20);

Unlike a HashMap, iteration is ordered by key.

Conceptually:

1
2
3
4
5
HashMap:
    unordered iteration

BTreeMap:
    ordered by key

52. BTreeSet

Similarly:

1
use std::collections::BTreeSet;

stores unique values in sorted order.

1
2
3
4
5
let mut values = BTreeSet::new();

values.insert(30);
values.insert(10);
values.insert(20);

Iteration produces:

1
2
3
10
20
30

53. Choosing Between HashMap and BTreeMap

A simple mental model:

1
2
3
4
5
6
7
8
HashMap<K, V>
    -> fast average lookup
    -> no sorted iteration requirement

BTreeMap<K, V>
    -> ordered keys
    -> range queries
    -> sorted iteration

Don’t choose based only on theoretical complexity. The actual workload, memory layout, cache behavior, and API requirements matter.


54. Collection Overview

The most important standard collections are:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Vec<T>
    Growable contiguous sequence

VecDeque<T>
    Double-ended queue

LinkedList<T>
    Doubly linked list

HashMap<K, V>
    Hash table

HashSet<T>
    Hash set

BTreeMap<K, V>
    Ordered map

BTreeSet<T>
    Ordered set

55. Contiguous vs Non-Contiguous Collections

This distinction is important for systems programming.

Vec<T> stores elements contiguously:

1
2
3
+----+----+----+----+----+
| T  | T  | T  | T  | T  |
+----+----+----+----+----+

This gives good cache locality.

A linked list is conceptually:

1
2
3
+------+       +------+       +------+
| Node | ----> | Node | ----> | Node |
+------+       +------+       +------+

The nodes can be located in different areas of memory.

This affects performance significantly.


56. Slices as Abstractions

One of the most useful Rust patterns is to accept slices in APIs.

Instead of:

1
2
3
fn process(values: Vec<i32>) {
    // ...
}

you can often write:

1
2
3
fn process(values: &[i32]) {
    // ...
}

Now the caller doesn’t need to give up ownership.

It can pass:

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

process(&values);

or:

1
2
3
let values = [1, 2, 3];

process(&values);

This makes the API more general.


57. &[T] Is Similar to &str

There is a useful analogy:

1
2
Vec<T>       -> &[T]
String       -> &str

In both cases:

1
2
3
4
5
owned value
    |
    | borrow
    v
borrowed view

For example:

1
2
3
4
5
Vec<T>
    -> &[T]

String
    -> &str

The borrowed versions do not own the underlying data.


58. Ownership Comparison

A useful summary:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[T; N]
    owns fixed-size data

Vec<T>
    owns growable data

&[T]
    borrows a sequence

String
    owns growable UTF-8 data

&str
    borrows UTF-8 string data

HashMap<K, V>
    owns key-value entries

Tuple
    owns its fields

Struct
    owns its fields

59. Copying vs Borrowing

Consider:

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

let other = values;

values is moved.

You cannot use:

1
println!("{:?}", values);

afterward.

But:

1
2
3
4
5
6
let values = vec![1, 2, 3];

let slice = &values[..];

println!("{:?}", values);
println!("{:?}", slice);

works because slice only borrows the vector.

This is one of the fundamental reasons slices are so useful.


60. Passing Collections to Functions

Ownership:

1
2
3
fn consume(values: Vec<i32>) {
    // takes ownership
}

Borrow:

1
2
3
fn read(values: &[i32]) {
    // borrows
}

Mutable borrow:

1
2
3
fn modify(values: &mut [i32]) {
    // mutably borrows
}

The same idea applies to strings:

1
2
3
4
5
fn consume(name: String) {}

fn read(name: &str) {}

fn modify(name: &mut String) {}

61. A Useful Type Hierarchy

For sequences:

1
2
3
4
5
6
7
8
9
10
11
                 sequence
                    |
        +-----------+-----------+
        |                       |
    owned data              borrowed view
        |                       |
    +---+---+               +---+---+
    |       |               |       |
 [T; N]   Vec<T>          &[T]    &str
                             |
                           strings

More precisely, &str is specifically a borrowed UTF-8 string slice, while &[T] is a generic borrowed slice.


62. str Is Unsized

This is a useful connection to Rust’s Sized concept.

You normally don’t have:

1
let s: str;

because str does not have a known size at compile time.

Instead you use:

1
&str

A &str is a reference to dynamically sized string data.

Similarly:

1
[T]

is an unsized slice type.

You normally use:

1
&[T]

63. Arrays, Slices, and Sized

Compare:

1
[i32; 5]

The compiler knows:

1
size = 5 * size_of::<i32>()

So the type has a known size.

But:

1
[i32]

does not specify how many elements exist.

Therefore it is dynamically sized.

This is why we usually use:

1
&[i32]

instead of:

1
[i32]

64. Indexing vs Iterators

You can access elements using indexing:

1
2
3
let values = vec![10, 20, 30];

let value = values[1];

But Rust code often prefers iterators for processing:

1
let sum: i32 = values.iter().sum();

Or:

1
2
3
4
let doubled: Vec<i32> =
    values.iter()
        .map(|x| x * 2)
        .collect();

This works naturally with slices and many other collections.


65. iter() vs iter_mut() vs into_iter()

This is important when working with collections.

iter()

Borrow elements:

1
2
3
for value in values.iter() {
    println!("{value}");
}

Conceptually:

1
&T

iter_mut()

Mutably borrow elements:

1
2
3
for value in values.iter_mut() {
    *value += 1;
}

Conceptually:

1
&mut T

into_iter()

Consume the collection:

1
2
3
for value in values.into_iter() {
    println!("{value}");
}

Conceptually:

1
T

So:

1
2
3
4
5
6
7
8
iter()
    -> &T

iter_mut()
    -> &mut T

into_iter()
    -> T

This is closely connected to Rust’s ownership system.


66. String and Collection Iteration

For strings:

1
let text = String::from("hello");

You can iterate over Unicode scalar values:

1
2
3
for character in text.chars() {
    println!("{character}");
}

Or UTF-8 bytes:

1
2
3
for byte in text.bytes() {
    println!("{byte}");
}

Don’t confuse:

1
2
3
4
5
bytes()
    -> UTF-8 bytes

chars()
    -> Unicode scalar values

67. Formatting Collections

For debugging:

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

println!("{:?}", values);

For pretty debugging:

1
println!("{:#?}", values);

Many Rust collections implement Debug, making this very useful during development.


68. String Is Not Vec<char>

This is an important misconception.

It is tempting to think:

1
String = Vec<char>

but that’s not how Rust strings work.

A String stores UTF-8 encoded bytes.

For example, one visible character can require multiple bytes.

Therefore:

1
2
3
let s = String::from("é");

println!("{}", s.len());

does not necessarily equal:

1
1

because len() reports the number of bytes, not the number of Unicode characters.


69. len() Means Different Things

Be careful with len().

For:

1
Vec<T>

it means:

1
number of elements

For:

1
String

it means:

1
number of UTF-8 bytes

For:

1
&str

it also means:

1
number of UTF-8 bytes

For:

1
HashMap

it means:

1
number of key-value entries

70. Core Data Structures Cheat Sheet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
┌─────────────────────┬──────────────────────────────┐
│ Type                │ Meaning                      │
├─────────────────────┼──────────────────────────────┤
│ [T; N]              │ Fixed-size array             │
│ Vec<T>              │ Growable array                │
│ &[T]                │ Borrowed slice                │
│ &mut [T]             │ Mutable borrowed slice       │
│ String              │ Owned growable UTF-8 string   │
│ &str                │ Borrowed UTF-8 string slice   │
│ (T1, T2, ...)        │ Tuple                        │
│ ()                  │ Unit type                    │
│ struct              │ Custom data structure        │
│ enum                │ One of several variants      │
│ HashMap<K, V>        │ Hash table                   │
│ HashSet<T>           │ Hash set                     │
│ BTreeMap<K, V>       │ Ordered map                  │
│ BTreeSet<T>          │ Ordered set                  │
│ VecDeque<T>          │ Double-ended queue           │
│ LinkedList<T>        │ Doubly linked list           │
└─────────────────────┴──────────────────────────────┘

71. The Most Important Relationships

For Rust development, especially systems programming, remember these relationships:

1
2
3
4
5
6
Array
    [T; N]
       |
       | borrow
       v
    &[T]
1
2
3
4
5
Vec<T>
    |
    | borrow
    v
&[T]
1
2
3
4
5
String
    |
    | borrow
    v
&str

And:

1
2
3
4
5
6
7
Vec<T>
    |
    +-- iter()      -> &T
    |
    +-- iter_mut()  -> &mut T
    |
    +-- into_iter() -> T

72. Choosing the Right Structure

A practical decision tree:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Do I know the size at compile time?
        |
       yes
        |
        v
     [T; N]

       no
        |
        v
Do I need a contiguous growable sequence?
        |
       yes
        |
        v
     Vec<T>

For a borrowed sequence:

1
2
3
4
5
6
Do I need to own the elements?
        |
       no
        |
        v
     &[T]

For strings:

1
2
3
4
5
6
7
8
9
10
11
12
13
Do I need an owned growable UTF-8 string?
        |
       yes
        |
        v
     String

Do I only need to read a string?
        |
       yes
        |
        v
     &str

For key-value lookup:

1
2
3
4
5
6
7
Do I need key -> value lookup?
        |
       yes
        |
        +-- no ordering needed -> HashMap
        |
        +-- ordered keys        -> BTreeMap

For unique values:

1
2
3
4
5
HashSet
    -> uniqueness

BTreeSet
    -> uniqueness + ordering

For queue behavior:

1
2
VecDeque
    -> efficient front/back operations

73. A Systems Programming Perspective

When working with low-level Rust, the most important distinction is often not simply “which collection?”

It is:

1
2
3
4
5
6
7
8
9
10
11
12
13
Who owns the memory?
        |
        v
Who can mutate it?
        |
        v
How long can the reference live?
        |
        v
Is the data contiguous?
        |
        v
Is its size known at compile time?

For example:

1
2
3
fn process(buffer: &[u8]) {
    // ...
}

This function says:

1
2
3
4
I don't own the buffer.
I only need read access.
The buffer is contiguous.
I don't care whether it came from an array or Vec.

While:

1
2
3
fn process(buffer: Vec<u8>) {
    // ...
}

says:

1
I take ownership of the vector.

And:

1
2
3
fn process(buffer: &mut [u8]) {
    // ...
}

says:

1
2
3
I don't own the buffer.
I need mutable access.
The data is contiguous.

This distinction becomes particularly important when working with networking, storage, virtualization, device I/O, and memory management.


74. Key Takeaways

The most important types to remember are:

1
[T; N]

Fixed-size array.

1
Vec<T>

Owned, growable contiguous collection.

1
&[T]

Borrowed view over contiguous elements.

1
String

Owned, growable UTF-8 string.

1
&str

Borrowed UTF-8 string slice.

1
(T1, T2, ...)

Heterogeneous fixed-size tuple.

1
()

Unit type representing one empty value.

1
HashMap<K, V>

Key-value lookup.

1
HashSet<T>

Unique values.

And remember the two especially useful API relationships:

1
2
Vec<T>  -> &[T]
String  -> &str

The owned type owns the data; the slice provides a borrowed view.

For functions that only need to inspect data, accepting a borrowed slice such as:

1
fn process(values: &[T])

or a string slice:

1
fn process(text: &str)

often produces a more flexible API than taking ownership of Vec<T> or String.

This post is licensed under CC BY 4.0 by the author.