Collections
Swift and Rust share the same trio of workhorse collections – dynamic arrays, hash maps, and hash sets – but the details of how they work differ in important ways. Rust also has a pair of types with no direct Swift equivalent: fixed-size arrays that live on the stack and slices that borrow a contiguous region of another collection.
Dynamic arrays: Vec<T> and Array
Section titled “Dynamic arrays: Vec<T> and Array”Rust’s Vec<T> is the equivalent of Swift’s Array<Element>. Both are growable, heap-allocated, contiguous sequences of values.
Creation
Section titled “Creation”// Swiftvar numbers: [Int] = [1, 2, 3]let empty: [String] = []let repeated = Array(repeating: 0, count: 5)// Rustlet mut numbers: Vec<i32> = vec![1, 2, 3];let empty: Vec<String> = Vec::new();let repeated = vec![0; 5];The vec![] macro is the idiomatic way to create a Vec with initial values. It plays the same role as Swift’s array literal syntax. Vec::new() creates an empty vector, just as [] does in Swift.
You can also build a Vec from a range or an iterator:
// Rustlet range_vec: Vec<i32> = (1..=5).collect(); // [1, 2, 3, 4, 5]let from_iter: Vec<i32> = [10, 20, 30].into_iter().collect();Accessing elements
Section titled “Accessing elements”Both languages provide subscript access, but Rust panics on out-of-bounds access just as Swift does:
// Swiftlet first = numbers[0] // 1let maybe = numbers.first // Optional<Int>// Rustlet first = numbers[0]; // 1 – panics if emptylet maybe = numbers.first(); // Option<&i32>Notice that first() in Rust returns an Option<&T> – a reference to the element inside the vector. Swift’s .first returns an Optional<Element> that is a copy of the value (for value types) or a reference (for reference types).
Mutation
Section titled “Mutation”// Swiftnumbers.append(4)numbers.insert(0, at: 0)let removed = numbers.remove(at: 2)numbers[0] = 99// Rustnumbers.push(4);numbers.insert(0, 0);let removed = numbers.remove(2);numbers[0] = 99;Swift uses append and remove(at:) with argument labels; Rust uses push and remove with positional arguments. The naming difference reflects Rust’s convention of shorter method names without labels.
Common methods
Section titled “Common methods”// Swiftnumbers.countnumbers.isEmptynumbers.contains(3)numbers.sort()numbers.reverse()let sorted = numbers.sorted()// Rustnumbers.len()numbers.is_empty()numbers.contains(&3)numbers.sort();numbers.reverse();let mut sorted = numbers.clone();sorted.sort();A few things stand out. Rust uses len() rather than count. The contains method takes a reference (&3) because it compares by borrowing, not by consuming elements. And Rust has no built-in sorted() that returns a new vector – you clone and sort in place, or use iterators (covered in the next chapter on iterators).
Capacity and performance
Section titled “Capacity and performance”Both Vec and Array use a growth strategy that amortizes appends to O(1). You can pre-allocate capacity in both languages:
// Swiftvar buffer: [Int] = []buffer.reserveCapacity(1000)// Rustlet mut buffer: Vec<i32> = Vec::with_capacity(1000);In Rust, Vec::with_capacity allocates memory for 1,000 elements without initializing them. buffer.len() is still 0, but buffer.capacity() is at least 1,000.
Fixed-size arrays: [T; N]
Section titled “Fixed-size arrays: [T; N]”Rust has a separate type for fixed-size arrays: [T; N], where N is a compile-time constant. These live on the stack (unless explicitly boxed) and have no heap allocation.
// Rustlet rgb: [u8; 3] = [255, 128, 0];let zeros = [0i32; 10]; // ten zerosSwift does not have a direct equivalent. Swift’s Array is always heap-allocated and dynamically sized. The closest Swift analog would be a tuple like (UInt8, UInt8, UInt8), but tuples lack the collection API.
Fixed-size arrays in Rust support indexing, iteration, and comparison. The size is part of the type, so [u8; 3] and [u8; 4] are different types and cannot be assigned to each other:
// Rustlet a: [i32; 3] = [1, 2, 3];let b: [i32; 4] = [1, 2, 3, 4];// a = b; // compile error: mismatched typesFixed-size arrays are useful for data with a known, constant length – pixel components, cryptographic hashes, matrix dimensions, or small lookup tables.
Slices: &[T]
Section titled “Slices: &[T]”A slice &[T] is a borrowed view into a contiguous sequence of T values. It consists of a pointer and a length – no ownership, no allocation. Slices can refer to part or all of a Vec, a fixed-size array, or another slice.
// Rustlet numbers = vec![10, 20, 30, 40, 50];let middle: &[i32] = &numbers[1..4]; // [20, 30, 40]let all: &[i32] = &numbers; // the whole vector as a sliceSwift’s closest equivalent is ArraySlice:
// Swiftlet numbers = [10, 20, 30, 40, 50]let middle = numbers[1..<4] // ArraySlice<Int> containing [20, 30, 40]There is an important difference: Swift’s ArraySlice retains the original array’s indices (so middle.startIndex is 1, not 0), while a Rust slice always starts at index 0. This is a common source of confusion for Swift developers.
Slices are the preferred way to pass collections to functions when you only need to read the data. Because they borrow rather than own, they avoid copies and work with any contiguous source:
// Rustfn sum(values: &[i32]) -> i32 { values.iter().sum()}
fn main() { let v = vec![1, 2, 3]; let a = [4, 5, 6];
println!("{}", sum(&v)); // pass a Vec as a slice println!("{}", sum(&a)); // pass a fixed-size array as a slice}This is similar to how you might write a Swift function that accepts some Sequence<Int>, except that slices are specific to contiguous memory and carry no protocol witness overhead.
Mutable slices (&mut [T]) allow modifying the underlying data:
// Rustfn double_all(values: &mut [i32]) { for v in values.iter_mut() { *v *= 2; }}
fn main() { let mut data = vec![1, 2, 3]; double_all(&mut data); println!("{:?}", data); // [2, 4, 6]}Hash maps: HashMap<K, V> and Dictionary
Section titled “Hash maps: HashMap<K, V> and Dictionary”Rust’s HashMap<K, V> is the equivalent of Swift’s Dictionary<Key, Value>. Both are unordered collections of key-value pairs with O(1) average-case lookups.
Creation
Section titled “Creation”// Swiftvar scores: [String: Int] = ["Alice": 10, "Bob": 7]let empty: [String: Int] = [:]// Rustuse std::collections::HashMap;
let mut scores: HashMap<&str, i32> = HashMap::from([ ("Alice", 10), ("Bob", 7),]);let empty: HashMap<String, i32> = HashMap::new();Unlike Vec, HashMap is not in the prelude – you must bring it into scope with use std::collections::HashMap. There is no built-in macro like vec![] for hash maps, but HashMap::from accepts an array of tuples.
Access and insertion
Section titled “Access and insertion”// Swiftlet aliceScore = scores["Alice"] // Optional<Int>scores["Charlie"] = 5scores["Alice"] = nil // removes the entry// Rustlet alice_score = scores.get("Alice"); // Option<&i32>scores.insert("Charlie", 5);scores.remove("Alice");Swift overloads subscript for both reading (returns optional) and writing (including deletion via assigning nil). Rust separates these into distinct methods: get for lookup, insert for insertion, and remove for deletion.
The entry API
Section titled “The entry API”Rust’s Entry API provides a concise way to insert a value only if the key is missing – a common pattern that Swift handles with Dictionary’s subscript with default:
// Swiftscores["Dave", default: 0] += 1// Rust*scores.entry("Dave").or_insert(0) += 1;The entry method returns an Entry enum that lets you inspect whether the key exists and act accordingly, all without doing two separate lookups.
Iteration
Section titled “Iteration”// Swiftfor (name, score) in scores { print("\(name): \(score)")}// Rustfor (name, score) in &scores { println!("{name}: {score}");}As with dictionaries in Swift, iteration order is not guaranteed.
Key requirements
Section titled “Key requirements”In Swift, dictionary keys must conform to Hashable. In Rust, keys must implement both Hash and Eq. Most primitive types and String satisfy these requirements. If you need a custom struct as a key, derive the necessary traits:
// Rust#[derive(Hash, Eq, PartialEq)]struct Point { x: i32, y: i32,}Hash sets: HashSet<T> and Set
Section titled “Hash sets: HashSet<T> and Set”Rust’s HashSet<T> is equivalent to Swift’s Set<Element>. It stores unique values with O(1) average-case lookups.
// Swiftvar tags: Set<String> = ["rust", "swift", "wasm"]tags.insert("go")tags.contains("rust") // true// Rustuse std::collections::HashSet;
let mut tags: HashSet<&str> = HashSet::from(["rust", "swift", "wasm"]);tags.insert("go");tags.contains("rust"); // trueBoth languages support the standard set operations:
// Swiftlet a: Set = [1, 2, 3, 4]let b: Set = [3, 4, 5, 6]
a.union(b) // {1, 2, 3, 4, 5, 6}a.intersection(b) // {3, 4}a.subtracting(b) // {1, 2}a.symmetricDifference(b) // {1, 2, 5, 6}// Rustuse std::collections::HashSet;
let a: HashSet<i32> = HashSet::from([1, 2, 3, 4]);let b: HashSet<i32> = HashSet::from([3, 4, 5, 6]);
let union: HashSet<_> = a.union(&b).cloned().collect();let intersection: HashSet<_> = a.intersection(&b).cloned().collect();let difference: HashSet<_> = a.difference(&b).cloned().collect();let symmetric: HashSet<_> = a.symmetric_difference(&b).cloned().collect();The Rust versions return iterators rather than new sets, so you call .cloned().collect() to materialize the results. The set operation methods borrow a and b, so both remain usable. You can also use the operator overloads & (intersection), | (union), - (difference), and ^ (symmetric difference) on HashSet references:
// Rustlet also_union: HashSet<_> = &a | &b;let also_intersection: HashSet<_> = &a & &b;Ownership and collections
Section titled “Ownership and collections”When you add a value to a collection in Rust, the collection takes ownership of that value. This is the most important difference from Swift for day-to-day programming.
// Rustlet name = String::from("Alice");let mut names = Vec::new();names.push(name);// println!("{name}"); // compile error: value moved into the vectorAfter push, the string has been moved into the vector and name is no longer usable. In Swift, Array.append copies value types and retains reference types – the original variable remains valid either way.
The same applies to HashMap and HashSet. If you need to keep using the original value, you can clone it or store references:
// Rust – cloninglet name = String::from("Alice");let mut names = Vec::new();names.push(name.clone());println!("{name}"); // still valid
// Rust – storing references (requires lifetime management)let name = String::from("Alice");let mut names: Vec<&str> = Vec::new();names.push(&name);println!("{name}"); // still validWhen you remove a value from a collection, ownership transfers back to you:
// Rustlet mut stack = vec![1, 2, 3];let top = stack.pop(); // Option<i32> – you now own the valueThis ownership model means that Rust collections never share mutable state implicitly. Combined with the borrowing rules, this prevents a whole class of bugs that Swift addresses through copy-on-write semantics.
Key differences and gotchas
Section titled “Key differences and gotchas”VecvsArraynaming: Rust’sVecis Swift’sArray. Rust’s[T; N](fixed-size array) has no Swift equivalent. Be careful not to confuse them.- No implicit copying: adding a value to a Rust collection moves it. Use
.clone()if you need to keep the original. - Imports required:
HashMapandHashSetmust be imported fromstd::collections.Vecis in the prelude and needs no import. - References in access methods: many
VecandHashMapmethods return references (&T) rather than owned values, since the collection still owns the data. - Slice indexing: Rust slices are always zero-indexed, unlike Swift’s
ArraySlicewhich preserves original indices. - No subscript sugar for maps: Rust does not overload
[]for optional access onHashMap. Useget()for safe lookups and[]only when you are certain the key exists (it panics otherwise). containstakes a reference:vec.contains(&value)requires passing a reference, not the value itself.