Vectors in Rust
In Rust, a vector (Vec<T>
) is a growable, heap-allocated array. If you’re coming from JavaScript, vectors are similar to JavaScript arrays but with strong typing and additional memory safety guarantees.
Creating Vectors
Let’s compare how we create arrays in JavaScript versus vectors in Rust:
// JavaScript array creationlet emptyArray = [];let numbers = [1, 2, 3, 4, 5];let mixedTypes = [1, 'hello', true]; // JavaScript arrays can mix types
In Rust:
// Empty vector with type annotationlet empty_vec: Vec<i32> = Vec::new();
// Vector with initial values using vec! macrolet numbers = vec![1, 2, 3, 4, 5];
// In Rust, all elements must be of the same type// This won't compile:// let mixed_types = vec![1, "hello", true];
// You can use Vec::with_capacity for performance when you know the sizelet with_capacity = Vec::with_capacity(10); // Allocates space for 10 elements
Key differences:
- Rust vectors must contain elements of the same type
- In Rust, you specify the type (
Vec<T>
) whereT
is the element type - The
vec!
macro is a convenient way to create vectors with initial values - Rust offers
with_capacity
for performance optimization
Adding Elements to Vectors
JavaScript:
let fruits = ['apple', 'banana'];fruits.push('orange');fruits.unshift('strawberry'); // Add to frontconsole.log(fruits); // ['strawberry', 'apple', 'banana', 'orange']
Rust:
let mut fruits = vec!["apple", "banana"];fruits.push("orange");// No direct equivalent to unshift, but you can use insertfruits.insert(0, "strawberry");println!("{:?}", fruits); // ["strawberry", "apple", "banana", "orange"]
Key differences:
- Rust vectors must be declared as mutable (
mut
) to be modified - No direct
unshift
equivalent, butinsert
can be used with index 0 - Rust requires the same type for all elements added to the vector
Accessing Vector Elements
JavaScript:
let numbers = [10, 20, 30, 40, 50];let first = numbers[0]; // 10let nonExistent = numbers[100]; // undefined (no error)
Rust has two ways to access elements:
let numbers = vec![10, 20, 30, 40, 50];
// Method 1: Using indexing (panics if out of bounds)let first = numbers[0]; // 10// let will_panic = numbers[100]; // This would panic at runtime
// Method 2: Using get (returns Option<&T>)match numbers.get(0) { Some(value) => println!("First element: {}", value), None => println!("No element found"),}
// Or more concisely with if letif let Some(value) = numbers.get(100) { println!("Element at index 100: {}", value);} else { println!("No element at index 100");}
Key differences:
- JavaScript returns
undefined
for out-of-bounds access, while Rust’s[]
operator panics - Rust’s
get
method returns anOption<&T>
, forcing you to handle the possibility of an out-of-bounds access - This approach prevents the common “undefined is not a function” errors in JavaScript
Removing Elements
JavaScript:
let numbers = [1, 2, 3, 4, 5];let last = numbers.pop(); // 5let first = numbers.shift(); // 1numbers.splice(1, 1); // Removes 1 element at index 1 (3)console.log(numbers); // [2, 4]
Rust:
let mut numbers = vec![1, 2, 3, 4, 5];let last = numbers.pop(); // Some(5)// No direct shift method, but we can use removelet first = numbers.remove(0); // 1numbers.remove(1); // Removes element at index 1 (3)println!("{:?}", numbers); // [2, 4]
Key differences:
- Rust’s
pop
returns anOption<T>
since the vector might be empty - Rust has no direct
shift
equivalent, butremove(0)
achieves the same result - Rust’s
remove
is similar to JavaScript’ssplice
when removing a single element
Iterating Over Vectors
JavaScript:
let numbers = [1, 2, 3, 4, 5];
// For...of loopfor (const num of numbers) { console.log(num);}
// forEach methodnumbers.forEach(num => console.log(num));
// Map to create a new arraylet doubled = numbers.map(n => n * 2); // [2, 4, 6, 8, 10]
// Filter to create a subsetlet evens = numbers.filter(n => n % 2 === 0); // [2, 4]
Rust:
let numbers = vec![1, 2, 3, 4, 5];
// Basic for loop (immutable references)for num in &numbers { println!("{}", num);}
// Iterating with mutable referenceslet mut nums = vec![1, 2, 3, 4, 5];for num in &mut nums { *num *= 2; // Dereference to modify}println!("{:?}", nums); // [2, 4, 6, 8, 10]
// Using iterator methods// Maplet doubled: Vec<i32> = numbers.iter().map(|n| n * 2).collect();println!("{:?}", doubled); // [2, 4, 6, 8, 10]
// Filterlet evens: Vec<i32> = numbers.iter().filter(|&&n| n % 2 == 0).cloned().collect();println!("{:?}", evens); // [2, 4]
Key differences:
- Rust’s iterator methods (
map
,filter
, etc.) don’t modify the original vector - You need to call
collect()
to create a new vector from an iterator - Rust requires you to specify if you want references (
&
) or mutable references (&mut
) - Dereferencing with
*
is needed to modify elements through mutable references
Using for
Loop with Indices
JavaScript:
let fruits = ['apple', 'banana', 'orange'];
// Using indexfor (let i = 0; i < fruits.length; i++) { console.log(`${i}: ${fruits[i]}`);}
// Using entries() methodfor (const [index, fruit] of fruits.entries()) { console.log(`${index}: ${fruit}`);}
Rust:
let fruits = vec!["apple", "banana", "orange"];
// Using indexfor i in 0..fruits.len() { println!("{}: {}", i, fruits[i]);}
// Using enumeratefor (index, fruit) in fruits.iter().enumerate() { println!("{}: {}", index, fruit);}
Advanced Vector Patterns
Storing Different Types with Enums
In JavaScript, arrays can store different types directly:
let mixed = [42, "hello", true];
In Rust, vectors must contain elements of the same type, but you can use enums to store different types:
enum SpreadsheetCell { Int(i32), Float(f64), Text(String),}
let row = vec![ SpreadsheetCell::Int(42), SpreadsheetCell::Text(String::from("hello")), SpreadsheetCell::Float(3.14),];
// Access with pattern matchingfor cell in &row { match cell { SpreadsheetCell::Int(i) => println!("Integer: {}", i), SpreadsheetCell::Float(f) => println!("Float: {}", f), SpreadsheetCell::Text(s) => println!("Text: {}", s), }}
Extending Vectors
JavaScript:
let arr1 = [1, 2, 3];let arr2 = [4, 5, 6];let combined = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6]
// Or using concatlet alsoCombined = arr1.concat(arr2);
Rust:
let vec1 = vec![1, 2, 3];let vec2 = vec![4, 5, 6];
// Using extendlet mut combined = vec1.clone();combined.extend(vec2.clone());println!("{:?}", combined); // [1, 2, 3, 4, 5, 6]
// Or creating a new vector (less efficient)let mut all = Vec::new();all.extend(&vec1);all.extend(&vec2);
Vector Slices
JavaScript uses array methods or the slice method:
let numbers = [1, 2, 3, 4, 5];let slice = numbers.slice(1, 4); // [2, 3, 4]
Rust uses slice references:
let numbers = vec![1, 2, 3, 4, 5];let slice = &numbers[1..4]; // [2, 3, 4]
// Slices are references to a portion of the vectorprintln!("{:?}", slice);
// You can create a new vector from a slicelet new_vec = slice.to_vec();
Key difference: Rust slices are references to the original vector, not new allocations.
Performance Considerations
Capacity and Reallocation
In JavaScript, you rarely manage array capacity directly. In Rust, you can optimize vector performance:
// Pre-allocate capacitylet mut with_capacity = Vec::with_capacity(1000);
// Add elements without triggering reallocationfor i in 0..1000 { with_capacity.push(i);}
// Check capacity vs lengthprintln!("Length: {}, Capacity: {}", with_capacity.len(), with_capacity.capacity());
// Shrink to fit actual sizewith_capacity.shrink_to_fit();
Memory Safety
Rust’s ownership system prevents common errors:
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0]; // Immutable reference
// This would cause a compile error:// v.push(6); // Can't modify while references exist
println!("First: {}", first);// Now we can modifyv.push(6);
This prevents scenarios in JavaScript where an array could be modified while you’re iterating over it.
Dropping Vectors
In JavaScript, arrays are garbage-collected:
let arr = [1, 2, 3];arr = null; // Array may be garbage collected
In Rust, vectors are dropped when they go out of scope:
{ let v = vec![1, 2, 3]; // Do something with v} // v goes out of scope and is freed here
When a vector is dropped, all its elements are also dropped.
Working with Large Vectors
For large datasets, JavaScript might use specialized methods:
const largeArray = Array(1000000).fill(0);// Process in chunksfor (let i = 0; i < largeArray.length; i += 1000) { const chunk = largeArray.slice(i, i + 1000); // Process chunk}
Rust can use similar approaches but with more control:
let large_vec = vec![0; 1000000]; // Create vector with 1,000,000 zeros
// Process in chunksfor chunk in large_vec.chunks(1000) { // Process chunk println!("Chunk length: {}", chunk.len());}
// Or process in parallel using rayon// use rayon::prelude::*;// large_vec.par_chunks(1000).for_each(|chunk| {// // Process chunk in parallel// });
Summary
Vectors in Rust are similar to JavaScript arrays in purpose but with important differences:
Feature | JavaScript Array | Rust Vector |
---|---|---|
Type safety | Can mix types | All elements must be same type |
Growth | Automatic | Automatic but controllable |
Out-of-bounds access | Returns undefined | Panics with [] , returns Option with get() |
Mutability | Always mutable | Must be declared mutable |
Memory management | Garbage collected | Automatically freed when out of scope |
Performance control | Limited | Fine-grained control over capacity |
Understanding these differences helps JavaScript developers leverage Rust’s vector type effectively while maintaining memory safety and performance.
Next Steps
Now that you understand how to work with vectors in Rust, let’s explore another important collection type: Strings. In the next section, we’ll learn about Rust’s string types and how they compare to JavaScript strings.