Vectors and Slices
Data · Lesson 26
Vec<T> is a list that can grow. &[T], called a slice, is a borrowed window into part of one.
Coming from GoVec<T> is a slice that you own: v.push(x) replaces v = append(v, x) and changes the Vec in place. &w[1..3] is the same no-copy window as w[1:3], but it is read-only unless you ask for &mut, it has no capacity to append into, and the compiler refuses to push to w while the window is in use. That rules out the append aliasing surprise.
Coming from KotlinVec<T> is your MutableList, and &w[1..3] is a view like subList(1, 3). While the view is in use, the compiler refuses to add to w, where on the JVM subList can throw ConcurrentModificationException later. A Vec without mut is a list you cannot push to or assign into, which a Kotlin List does not promise.
Coming from PythonVec<T> is your list, but every item has the same type and there are no negative indexes. &w[1..3] is a borrowed view, not a copy like Python's slicing, and an index past the end panics (Python raises IndexError).
Coming from TypeScriptVec<T> is your T[] and push adds to the end the same way. &w[1..3] is a borrowed view rather than a copy like slice, and an index past the end panics instead of giving undefined.
Coming from JavaVec<i32> is an ArrayList<Integer> whose items are plain numbers stored side by side (Java's List<int> does not compile). &w[1..3] is like subList(1, 3), but Rust refuses to compile code that changes the Vec while a slice of it is in use, where Java can throw ConcurrentModificationException at run time.
Coming from C#Vec<T> is List<T>, and &w[1..3] is a borrowed view like a Span<T>, not a copy like GetRange. While that view is alive, Rust won't let you push to the list.
Coming from CVec<T> is your pointer, length and capacity struct with push and realloc built in, and &[T] is a pointer plus a length. The compiler won't let you keep a pointer into a Vec across a push that might move it, and v[i] is bounds-checked.
Coming from C++Vec<T> is std::vector<T>, and &[T] is std::span<const T>. The difference is that holding a reference or span into a vector, calling push and then using the reference is a compile error, not undefined behaviour. v[i] checks its bounds where std::vector's operator[] does not by default.
The quick comparison
fn main() { let mut v: Vec<i32> = Vec::new(); v.push(10); v.push(20);
let mut w = vec![1, 2, 3]; // vec! makes a Vec with these values w.push(4);
let part: &[i32] = &w[1..3]; // a slice: a borrowed window, no copy println!("{v:?} {w:?} {part:?} len={}", w.len());}[10, 20] [1, 2, 3, 4] [2, 3] len=4
package main
import "fmt"
func main() { var v []int v = append(v, 10) v = append(v, 20)
w := []int{1, 2, 3} w = append(w, 4)
part := w[1:3] // a window onto w's array, no copy fmt.Printf("%v %v %v len=%d\n", v, w, part, len(w))}[10 20] [1 2 3 4] [2 3] len=4
fun main() { val v = mutableListOf<Int>() v.add(10) v.add(20)
val w = mutableListOf(1, 2, 3) w.add(4)
val part = w.subList(1, 3) println("$v $w $part len=${w.size}")}[10, 20] [1, 2, 3, 4] [2, 3] len=4
v: list[int] = []v.append(10)v.append(20)
w = [1, 2, 3]w.append(4)
part = w[1:3] # a slice of a list is a new list (a copy)print(f"{v} {w} {part} len={len(w)}")[10, 20] [1, 2, 3, 4] [2, 3] len=4
const v: number[] = [];v.push(10);v.push(20);
const w = [1, 2, 3];w.push(4); // const stops reassigning w, not changing it
const part = w.slice(1, 3); // slice makes a copy, not a viewconsole.log(v, w, part, `len=${w.length}`);[ 10, 20 ] [ 1, 2, 3, 4 ] [ 2, 3 ] len=4
import java.util.ArrayList;import java.util.List;
public class Main { public static void main(String[] args) { List<Integer> v = new ArrayList<>(); v.add(10); v.add(20);
var w = new ArrayList<>(List.of(1, 2, 3)); w.add(4);
List<Integer> part = w.subList(1, 3); // a view into w, no copy System.out.println(v + " " + w + " " + part + " len=" + w.size()); }}[10, 20] [1, 2, 3, 4] [2, 3] len=4
var v = new List<int>();v.Add(10);v.Add(20);
List<int> w = [1, 2, 3]; // collection expressionw.Add(4);
var part = w.GetRange(1, 2); // items 1 and 2 (a copy)Console.WriteLine($"[{string.Join(", ", v)}] [{string.Join(", ", w)}] [{string.Join(", ", part)}] len={w.Count}");[10, 20] [1, 2, 3, 4] [2, 3] len=4
#include <stdio.h>#include <stdlib.h>
// C has no Vec: a growable array is a pointer, a length and a capacitystruct IntVec { int *data; size_t len, cap;};
void push(struct IntVec *v, int x) { if (v->len == v->cap) { v->cap = v->cap ? v->cap * 2 : 4; v->data = realloc(v->data, v->cap * sizeof(int)); } v->data[v->len++] = x;}
void print_ints(const int *xs, size_t len) { printf("["); for (size_t i = 0; i < len; i++) { printf(i ? ", %d" : "%d", xs[i]); } printf("]");}
int main(void) { struct IntVec v = {0}; push(&v, 10); push(&v, 20);
struct IntVec w = {0}; for (int i = 1; i <= 4; i++) push(&w, i);
const int *part = &w.data[1]; // a "slice": pointer + length, no copy size_t part_len = 2;
print_ints(v.data, v.len); printf(" "); print_ints(w.data, w.len); printf(" "); print_ints(part, part_len); printf(" len=%zu\n", w.len);
free(v.data); free(w.data); return 0;}[10, 20] [1, 2, 3, 4] [2, 3] len=4
#include <iostream>#include <span>#include <vector>
void print(std::span<const int> xs) { std::cout << "["; for (std::size_t i = 0; i < xs.size(); ++i) { std::cout << (i ? ", " : "") << xs[i]; } std::cout << "]";}
int main() { std::vector<int> v; v.push_back(10); v.push_back(20);
std::vector<int> w{1, 2, 3}; w.push_back(4);
std::span<const int> part = std::span(w).subspan(1, 2); // a view, no copy print(v); std::cout << " "; print(w); std::cout << " "; print(part); std::cout << " len=" << w.size() << "\n";}[10, 20] [1, 2, 3, 4] [2, 3] len=4
What's different
{:?}prints a whole Vec. Normal{}printing is not defined for lists.v[10]on a short Vec crashes the program with a panic.v.get(10)returns an Option instead.- A Vec is three numbers on the stack (pointer, length, capacity) plus the items on the heap. A slice
&[T]is two numbers: a pointer and a length. let x = v[0];copies a number out of aVec<i32>, but for aVec<String>it is a compile error: borrow with&v[0]or copy withv[0].clone().v.sort()needs items that can always be ranked, andf64cannot (NaN). For floats, writev.sort_by(|a, b| a.total_cmp(b)).
Taking a String out of a list by index
Looks fine: in Go, Kotlin, Python, TypeScript, Java, C#, C and C++ it runs fine. Rust won't compile it.
Works in Go. In Rust it's just a rule.Works in Kotlin. In Rust it's just a rule.Works in Python. In Rust it's just a rule.Works in TypeScript. In Rust it's just a rule.Works in Java. In Rust it's just a rule.Works in C#. In Rust it's just a rule.Works in C. In Rust it's just a rule.Works in C++. In Rust it's just a rule.fn main() { let names = vec![String::from("ana"), String::from("bo")]; let first = names[0]; println!("{first} of {}", names.len());}package main
import "fmt"
func main() { names := []string{"ana", "bo"} first := names[0] fmt.Println(first, "of", len(names))}ana of 2
fun main() { val names = mutableListOf("ana", "bo") val first = names[0] println("$first of ${names.size}")}ana of 2
names = ["ana", "bo"]first = names[0]print(f"{first} of {len(names)}")ana of 2
const names = ["ana", "bo"];const first = names[0];console.log(`${first} of ${names.length}`);ana of 2
import java.util.ArrayList;import java.util.List;
public class Main { public static void main(String[] args) { List<String> names = new ArrayList<>(List.of("ana", "bo")); String first = names.get(0); System.out.println(first + " of " + names.size()); }}ana of 2
List<string> names = ["ana", "bo"];var first = names[0];Console.WriteLine($"{first} of {names.Count}");ana of 2
#include <stdio.h>
int main(void) { const char *names[] = {"ana", "bo"}; const char *first = names[0]; // copies the pointer, not the text printf("%s of %d\n", first, (int)(sizeof names / sizeof names[0])); return 0;}ana of 2
#include <iostream>#include <string>#include <vector>
int main() { std::vector<std::string> names{"ana", "bo"}; std::string first = names[0]; // copies the whole string std::cout << first << " of " << names.size() << "\n";}ana of 2
What does the compiler say? Guess first, then open.
error[E0507]: cannot move out of index of `Vec<String>` --> src/main.rs:3:17 | 3 | let first = names[0]; | ^^^^^^^^ move occurs because value has type `String`, which does not implement the `Copy` trait | help: consider borrowing here | 3 | let first = &names[0]; | + help: consider cloning the value if the performance cost is acceptable | 3 | let first = names[0].clone(); | ++++++++
Go prints ana of 2. A Go string is a small header (pointer and length) and its bytes never change, so first := names[0] copies the header and both names share the text safely.
Kotlin prints ana of 2. first and the list slot refer to the same String object. Strings can't be changed, so sharing is harmless, and the garbage collector keeps the object alive while either refers to it.
Python prints ana of 2. first and the list slot are two names for the same string object, which can't be changed, so sharing is harmless.
Prints ana of 2. first is a string value. Strings never change in place, so it makes no visible difference whether the engine copies or shares the characters.
Java prints ana of 2. first and the list slot refer to the same String object. A String can't be changed, and the garbage collector keeps it alive while either refers to it.
C# prints ana of 2. first and the list slot refer to the same string object, which can't be changed, and the garbage collector keeps it alive.
C prints ana of 2. first copies the pointer, so both point at the same text. Here the text is a string literal. With heap text, nothing in the code says which of the two pointers must free it, and freeing it through one leaves the other dangling.
C++ prints ana of 2. std::string first = names[0] copies the whole string, so the two are independent. To take it without copying you write std::move(names[0]), which leaves names[0] valid but unspecified.
names[0] is a slot inside the list's buffer, and the list owns the String that lives there. let first = names[0] would move the String out and leave a hole in the list. The list would still free every slot when it is dropped, so that String would be freed twice.
Nothing is wrong with wanting the first name. The other languages quietly choose for you: share a reference, copy a pointer, or copy the whole text. Rust asks which one you mean. &names[0] borrows it, names[0].clone() makes a second copy, names.remove(0) takes it out of the list, and std::mem::take(&mut names[0]) takes it and leaves an empty String behind. A Vec<i32> has no such error, because an i32 is copied, not moved. Any item that is not Copy behaves like the String, a struct or a row of a Vec<Vec<T>> included.
The fix
Compiles and runs.
fn main() { let names = vec![String::from("ana"), String::from("bo")]; let first = &names[0]; // borrow it; `names[0].clone()` would copy it println!("{first} of {}", names.len());}ana of 2
The toolkit
Outputs on this page come from Linux on 64-bit ARM with Rust 1.99.1. Where a fact depends on the platform, the text says so.
Making a Vec
A Vec<T> is a list of items that all have the same type T. Its items live on the heap, so it can grow and shrink while the program runs.
fn main() { let a = vec![10, 20, 30]; let b: Vec<i32> = Vec::new(); let c = vec![0; 4]; let d = Vec::from([1, 2, 3]); let e = Vec::<f64>::with_capacity(10); let f: Vec<u32> = (1..=5).collect();
println!("vec![10, 20, 30]: {a:?}"); println!("Vec::new(): {b:?}"); println!("vec![0; 4]: {c:?}"); println!("Vec::from([1, 2, 3]): {d:?}"); println!( "with_capacity(10): len {}, capacity {}", e.len(), e.capacity() ); println!("(1..=5).collect(): {f:?}");}vec![10, 20, 30]: [10, 20, 30] Vec::new(): [] vec![0; 4]: [0, 0, 0, 0] Vec::from([1, 2, 3]): [1, 2, 3] with_capacity(10): len 0, capacity 10 (1..=5).collect(): [1, 2, 3, 4, 5]
vec![…]is the usual way;vec![0; 4]makes four copies of0.Vec::new()makes an empty list. The compiler must learn the item type, from a written type (Vec<i32>) or a later line such as apush.with_capacity(n)is empty with room for at leastnitems (capacity and growth).collect()turns many things into aVec(iterators).
{:?} prints a whole list; plain {} is not defined for lists.
Adding, reading and removing
fn main() { let mut v = vec![10, 20, 30];
v.push(40); println!("after push(40): {v:?}"); println!("pop(): {:?}", v.pop()); println!("len: {}, is_empty: {}", v.len(), v.is_empty());
println!("v[1]: {}", v[1]); v[1] = 25; println!("after v[1] = 25: {v:?}");
println!("get(1): {:?}", v.get(1)); println!("get(10): {:?}", v.get(10)); println!("first(): {:?}, last(): {:?}", v.first(), v.last());
let mut empty: Vec<i32> = Vec::new(); println!( "empty.pop(): {:?}, empty.last(): {:?}", empty.pop(), empty.last() );}after push(40): [10, 20, 30, 40] pop(): Some(40) len: 3, is_empty: false v[1]: 20 after v[1] = 25: [10, 25, 30] get(1): Some(25) get(10): None first(): Some(10), last(): Some(30) empty.pop(): None, empty.last(): None
pushadds at the end andpoptakes the last item off.popreturns anOption, because the list may be empty.v[1]reads (andv[1] = 25writes) the item at position 1. The first item is position 0, and theVecmust belet mutto write.get(i),first()andlast()also return anOption: the way to ask for an item that might not be there.
Reading a position that does not exist is not an Option: v[i] panics, which stops the program with a message:
fn main() { let v = vec![10, 20, 30]; println!("v has {} items", v.len()); let i = 10; println!("v[{i}] is {}", v[i]);}v has 3 items thread 'main' (1) panicked at src/main.rs:5:31: index out of bounds: the len is 3 but the index is 10 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Safe Rust checks every index. Use v[i] when a missing item would be a bug, and get(i) when it is a normal case (user input, a lookup that might miss). The index must be a usize (an unsigned number the size of an address), not an i32:
fn main() { let v = vec![10, 20, 30]; let i: i32 = 1; println!("{}", v[i]);}error[E0277]: the type `[{integer}]` cannot be indexed by `i32` --> src/main.rs:4:22 | 4 | println!("{}", v[i]); | ^ slice indices are of type `usize` or ranges of `usize` | = help: the trait `SliceIndex<[{integer}]>` is not implemented for `i32` ... (notes pointing into the standard library trimmed)
Keep positions as usize from the start. What your language does for an index past the end is run in Your lists in Rust.
Changing the list
fn main() { let mut v = vec![10, 20, 30, 40, 50];
v.insert(1, 15); println!("insert(1, 15): {v:?}"); let removed = v.remove(0); println!("remove(0): {v:?}, returned {removed}"); let swapped = v.swap_remove(0); println!("swap_remove(0): {v:?}, returned {swapped}");
v.extend([60, 70]); println!("extend([60, 70]): {v:?}"); let mut more = vec![80, 90]; v.append(&mut more); println!("append(&mut more): {v:?}, more is now {more:?}");
v.truncate(4); println!("truncate(4): {v:?}"); v.retain(|x| *x != 30); println!("retain(*x != 30): {v:?}"); v.clear(); println!("clear(): {v:?}, capacity still {}", v.capacity());
let mut runs = vec![1, 1, 2, 2, 2, 1, 3, 3]; runs.dedup(); println!("dedup(): {runs:?}");}insert(1, 15): [10, 15, 20, 30, 40, 50] remove(0): [15, 20, 30, 40, 50], returned 10 swap_remove(0): [50, 20, 30, 40], returned 15 extend([60, 70]): [50, 20, 30, 40, 60, 70] append(&mut more): [50, 20, 30, 40, 60, 70, 80, 90], more is now [] truncate(4): [50, 20, 30, 40] retain(*x != 30): [50, 20, 40] clear(): [], capacity still 10 dedup(): [1, 2, 1, 3]
| Call | What it does | Notes |
|---|---|---|
insert(i, x) |
puts x at position i |
later items shift right; panics if i > len() |
remove(i) |
takes out item i and returns it |
later items shift left. It removes by position, not by value |
swap_remove(i) |
takes out item i and returns it |
the last item fills the gap: order changes, nothing shifts (O(1), say the docs) |
drain(a..b) |
removes positions a up to b and hands them to you |
the rest shift down |
extend(w) |
adds the items of w |
a Vec w is moved in (below). extend(&w) copies Copy items and keeps w |
append(&mut w) |
moves all of w’s items over |
leaves w empty |
truncate(n), clear(), resize(n, x) |
cut to n items, remove all, or cut or pad with x to exactly n |
truncate and clear keep the capacity |
retain(f) |
keeps the items for which f says true |
in place |
dedup() |
removes repeats next to each other | [1, 1, 2, 1] becomes [1, 2, 1]; sort first to remove all repeats |
reverse() |
flips the order | in place |
|x| *x != 30 is a closure, a small function written inline (closures). retain calls it per item with a reference, so *x is the number. remove and swap_remove hand you the removed item, which matters for lists of String (below).
extend(w) uses w up, so a later use of it is an error:
fn main() { let mut v = vec![1, 2]; let w = vec![3, 4]; v.extend(w); println!("{v:?} {w:?}");}error[E0382]: borrow of moved value: `w` --> src/main.rs:5:22 | 3 | let w = vec![3, 4]; | - move occurs because `w` has type `Vec<i32>`, which does not implement the `Copy` trait 4 | v.extend(w); | - value moved here 5 | println!("{v:?} {w:?}"); | ^ value borrowed here after move | help: consider cloning the value if the performance cost is acceptable | 4 | v.extend(w.clone()); | ++++++++
Looping
fn main() { let mut scores = vec![70, 85, 90];
for s in &scores { println!("read: {s}"); }
for s in &mut scores { *s += 5; } println!("after +5: {scores:?}");
for (i, s) in scores.iter().enumerate() { println!("score {i} is {s}"); }
let mut total = 0; for s in scores { total += s; } println!("total: {total}"); // `scores` was moved into the loop, so it cannot be used here.}read: 70 read: 85 read: 90 after +5: [75, 90, 95] score 0 is 75 score 1 is 90 score 2 is 95 total: 260
for s in &scoresborrows the list and gives a&i32per item. The list is still yours afterwards.for s in &mut scoresgives&mut i32, so*s += 5changes the item in place.for s in scores(no&) moves the list into the loop, so the last comment is true..iter().enumerate()adds the position. Iterators has the rest, includingsum,minandmax(iterator methods, notVecmethods); looping over collections covers the three loop forms.
Sorting and searching
fn main() { let mut nums = vec![30, 5, 12, 5, 99]; nums.sort(); println!("sort: {nums:?}"); nums.sort_by(|a, b| b.cmp(a)); println!("sort_by (reverse): {nums:?}");
let mut words = vec!["pear", "fig", "banana", "kiwi"]; words.sort_by_key(|w| w.len()); println!("sort_by_key(len): {words:?}");
let mut ranked = vec![("ana", 90), ("bo", 75), ("cy", 90), ("di", 75)]; ranked.sort_by_key(|p| p.1); println!("sort_by_key(score): {ranked:?}");
let mut quick = vec![3, 1, 2]; quick.sort_unstable(); println!("sort_unstable: {quick:?}");
let sorted = vec![5, 5, 12, 30, 99]; println!("contains(&12): {}", sorted.contains(&12)); println!("binary_search(&12): {:?}", sorted.binary_search(&12)); println!("binary_search(&13): {:?}", sorted.binary_search(&13)); println!("binary_search(&0): {:?}", sorted.binary_search(&0));}sort: [5, 5, 12, 30, 99]
sort_by (reverse): [99, 30, 12, 5, 5]
sort_by_key(len): ["fig", "pear", "kiwi", "banana"]
sort_by_key(score): [("bo", 75), ("di", 75), ("ana", 90), ("cy", 90)]
sort_unstable: [1, 2, 3]
contains(&12): true
binary_search(&12): Ok(2)
binary_search(&13): Err(3)
binary_search(&0): Err(0)sort()orders the items in place. It is stable: equal items keep their relative order (the docs say so).sort_unstable()may reorder equal items, which does not matter when they look the same.sort_by(|a, b| …)takes a closure that gets two references and returns anOrdering:Less,EqualorGreater.a.cmp(b)gives one, sob.cmp(a)puts the biggest first.sort_by_key(|w| w.len())sorts by a value worked out from each item.contains(&x)checks items one by one until it findsx.binary_search(&x)looks at far fewer on a long list but needs a sorted one; otherwise its answer means nothing. It returns aResult(errors):Ok(position), orErr(position)wherexcould be inserted to keep the order. With equal items, any of them may be returned.
Sorting floats
f64 cannot use plain sort():
fn main() { let mut temps = vec![21.5, 19.0, 23.25]; temps.sort(); println!("{temps:?}");}error[E0277]: the trait bound `{float}: Ord` is not satisfied --> src/main.rs:3:11 | 3 | temps.sort(); | ^^^^ the trait `Ord` is not implemented for `{float}` | = help: the following other types implement trait `Ord`: i128 i16 i32 i64 i8 isize u128 u16 and 4 others ... (notes pointing into the standard library trimmed)
Ord is a trait, a named ability a type has or lacks (traits): “any two values can be put in order”. f64 lacks it because of NaN (“not a number”), which is neither smaller than, larger than nor equal to anything, itself included. partial_cmp returns an Option of an Ordering, None for that case. total_cmp puts every f64 in one fixed order; use it with sort_by:
fn main() { let nan = f64::NAN; println!("NaN < 1.0: {}", nan < 1.0); println!("NaN > 1.0: {}", nan > 1.0); println!("NaN == NaN: {}", nan == nan); println!("partial_cmp: {:?}", nan.partial_cmp(&1.0));
let mut temps = vec![21.5, f64::NAN, 19.0, -0.0, 0.0, 23.25]; temps.sort_by(|a, b| a.total_cmp(b)); println!("total_cmp: {temps:?}");}NaN < 1.0: false NaN > 1.0: false NaN == NaN: false partial_cmp: None total_cmp: [-0.0, 0.0, 19.0, 21.5, 23.25, NaN]
a.partial_cmp(b).unwrap() works until a NaN is compared, and then panics. Strings sort by bytes, so capitals come first (strings).
Slices: a window onto a list
A slice, written &[T], is a borrowed view of items stored in a row: a pointer to the first one and a count. &v[1..3] is the items at positions 1 and 2 (the end is left out). Nothing is copied.
fn main() { let v = vec![10, 20, 30, 40, 50];
println!("&v[1..3]: {:?}", &v[1..3]); println!("&v[..2]: {:?}", &v[..2]); println!("&v[3..]: {:?}", &v[3..]); println!("&v[..]: {:?}", &v[..]); println!("get(3..9): {:?}", v.get(3..9));
let (left, right) = v.split_at(2); println!("split_at(2): {left:?} and {right:?}");
for pair in v.windows(2) { println!("window: {pair:?}"); } for chunk in v.chunks(2) { println!("chunk: {chunk:?}"); }
let rows = vec![vec![1, 2], vec![3], vec![4, 5]]; println!("concat: {:?}", rows.concat()); let words = vec!["a", "b", "c"]; println!("join: {}", words.join("-"));
let copy: Vec<i32> = v[1..3].to_vec(); println!("to_vec: {copy:?}");}&v[1..3]: [20, 30] &v[..2]: [10, 20] &v[3..]: [40, 50] &v[..]: [10, 20, 30, 40, 50] get(3..9): None split_at(2): [10, 20] and [30, 40, 50] window: [10, 20] window: [20, 30] window: [30, 40] window: [40, 50] chunk: [10, 20] chunk: [30, 40] chunk: [50] concat: [1, 2, 3, 4, 5] join: a-b-c to_vec: [20, 30]
- Leave out either end of the range for “from the start” or “to the end”.
&v[1..9]on a shorter list panics, likev[9].v.get(3..9)returnsNone.split_at,windowsandchunksare slice methods.windows(2)gives overlapping pairs,chunks(2)gives pieces that do not overlap, the last possibly shorter. Both panic on 0. More in iterators.concatglues a list of lists into one,join(sep)does it with something in between (strings too), andto_vec()copies a slice into a newVec.
A function that only reads takes &[i32]; one that changes items takes &mut [i32]. Both accept a Vec, an array, or part of either (Borrowing says why). Writing through &mut v[3..] changes v itself, because it is a view:
fn total(xs: &[i32]) -> i32 { let mut sum = 0; for x in xs { sum += x; } sum}
fn double_all(xs: &mut [i32]) { for x in xs { *x *= 2; }}
fn main() { let mut v = vec![1, 2, 3, 4, 5]; let arr = [10, 20, 30];
println!("total(&v): {}", total(&v)); println!("total(&arr): {}", total(&arr)); println!("total(&v[1..3]): {}", total(&v[1..3]));
double_all(&mut v[3..]); println!("after double_all(&mut v[3..]): {v:?}"); double_all(&mut v); println!("after double_all(&mut v): {v:?}");}total(&v): 15 total(&arr): 60 total(&v[1..3]): 5 after double_all(&mut v[3..]): [1, 2, 3, 8, 10] after double_all(&mut v): [2, 4, 6, 16, 20]
A slice has a length but no capacity, so it cannot push. The Vec that owns the items can, so a function that must grow a list takes &mut Vec<T> (Mutable borrows).
Arrays
An array, [T; N], has its length N in its type. It cannot grow, and needs no heap: its items sit in the variable.
use std::mem::size_of;
fn main() { println!("size_of::<[i32; 4]>(): {}", size_of::<[i32; 4]>()); println!("size_of::<[i32; 100]>(): {}", size_of::<[i32; 100]>()); println!("size_of::<Vec<i32>>(): {}", size_of::<Vec<i32>>()); println!("size_of::<&[i32]>(): {}", size_of::<&[i32]>());
let a = [1, 2, 3]; let b = a; // an array of Copy items is Copy println!("a: {a:?}, b: {b:?}");
let zeros = [0u8; 6]; println!("[0u8; 6]: {zeros:?}, len {}", zeros.len());
let v: Vec<i32> = a.to_vec(); println!("a.to_vec(): {v:?}");}size_of::<[i32; 4]>(): 16 size_of::<[i32; 100]>(): 400 size_of::<Vec<i32>>(): 24 size_of::<&[i32]>(): 16 a: [1, 2, 3], b: [1, 2, 3] [0u8; 6]: [0, 0, 0, 0, 0, 0], len 6 a.to_vec(): [1, 2, 3]
[i32; 4] |
Vec<i32> |
&[i32] |
|
|---|---|---|---|
| Length known | when compiling | when running | when running |
| Can grow | no | yes | no |
| Items stored | in the variable itself | on the heap | not owned: a view |
Is Copy |
yes, when the items are | no | yes (a reference) |
let b = a; above leaves a usable, because an array of Copy items is Copy. An array of Strings is not:
fn main() { let names = [String::from("ana"), String::from("bo")]; let other = names; println!("{names:?} {other:?}");}error[E0382]: borrow of moved value: `names` --> src/main.rs:4:16 | 2 | let names = [String::from("ana"), String::from("bo")]; | ----- move occurs because `names` has type `[String; 2]`, which does not implement the `Copy` trait 3 | let other = names; | ----- value moved here 4 | println!("{names:?} {other:?}"); | ^^^^^ value borrowed here after move | help: consider cloning the value if the performance cost is acceptable | 3 | let other = names.clone(); | ++++++++
Arrays are bounds-checked too. If the compiler can see the index is too big, a default-on lint (a built-in check, so no E code) refuses to build the program:
fn main() { let a = [10, 20, 30]; println!("{}", a[5]);}error: this operation will panic at runtime --> src/main.rs:3:20 | 3 | println!("{}", a[5]); | ^^^^ index out of bounds: the length is 3 but the index is 5 | = note: `#[deny(unconditional_panic)]` on by default
If the index is known only at run time, it is a panic, as with a Vec.
fn main() { let a = [10, 20, 30]; let i = a.len() + 2; println!("a[{i}] = {}", a[i]);}thread 'main' (1) panicked at src/main.rs:4:29: index out of bounds: the len is 3 but the index is 5 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Lists of lists
A grid is a Vec<Vec<T>>. vec![vec![0; 3]; 2] makes two rows of three zeros. The inner vec![0; 3] is cloned for each row, so the rows are independent.
fn main() { let mut grid = vec![vec![0; 3]; 2]; grid[1][2] = 7; println!("grid: {grid:?}"); println!("rows: {}, columns: {}", grid.len(), grid[0].len()); println!( "each row has its own buffer: {}", grid[0].as_ptr() != grid[1].as_ptr() );
// One flat Vec and some arithmetic: a single buffer. let (rows, cols) = (2, 3); let (row, col) = (1, 2); let mut flat = vec![0; rows * cols]; flat[row * cols + col] = 7; println!("flat: {flat:?}");
// Rows may have different lengths. let ragged = vec![vec![1], vec![2, 3], vec![]]; for (i, r) in ragged.iter().enumerate() { println!("row {i} has {} items", r.len()); }}grid: [[0, 0, 0], [0, 0, 7]] rows: 2, columns: 3 each row has its own buffer: true flat: [0, 0, 0, 0, 0, 7] row 0 has 1 items row 1 has 2 items row 2 has 0 items
Each non-empty row is its own heap block, so rows can differ in length, at the cost of one more pointer to follow.
Owning the items
let x = v[0] copies a number out, but a String is never copied by accident, so let first = names[0] is the gotcha at the top of the page (E0507, “cannot move out of index”). So is any item that is not Copy, a row of a Vec<Vec<T>> included. This is the rule from Ownership and Moves: a list owns its items, so taking one out must leave something valid behind. The ways to say what you mean:
use std::mem;
fn main() { let mut names = vec![ String::from("ana"), String::from("bo"), String::from("cy"), String::from("di"), ];
let looked_at: &String = &names[0]; println!("borrow: {looked_at}, list still has {}", names.len());
let copy = names[0].clone(); println!("clone: {copy}, list still has {}", names.len());
let taken = mem::take(&mut names[1]); println!("mem::take: {taken:?} out, {:?} left behind", names[1]);
names.swap(0, 3); println!("swap(0, 3): {names:?}");
let removed = names.remove(0); println!("remove(0): {removed}, list is now {names:?}");
let swapped = names.swap_remove(1); println!("swap_remove: {swapped}, list is now {names:?}");}borrow: ana, list still has 4 clone: ana, list still has 4 mem::take: "bo" out, "" left behind swap(0, 3): ["di", "", "cy", "ana"] remove(0): di, list is now ["", "cy", "ana"] swap_remove: cy, list is now ["", "ana"]
| You want | Use | What the list is left with |
|---|---|---|
| to look at it | &names[0] |
unchanged |
| a second copy | names[0].clone() |
unchanged |
| the item, and the gap closed | names.remove(0) |
one shorter, the rest shift down |
| the item, and the order may change | names.swap_remove(0) |
one shorter, the last item fills the gap |
| the item, and the position kept | std::mem::take(&mut names[0]) |
an empty String in that position |
mem::take and mem::swap are in Mutable borrows, which also covers split_at_mut for two &mut items at once.
Your lists in Rust
Pick your language in “Coming from” at the top of the page. Rows with iter() or |x| use iterators and closures, covered later: read them as “the position of x” and “keep the items where…”.
Going deeper
Where the bytes live
An array’s items are in the variable. A Vec is a small header on the stack and the items on the heap. A slice is a pointer and a length, and can point into either. (The order of the header’s fields is not promised; the pictures use a convenient one.)
a and b have the same type and size, whether they look at the stack or the heap. Neither owns the items.use std::mem::size_of_val;
fn main() { let arr = [10, 20, 30, 40]; let v = vec![10, 20, 30, 40]; let from_arr: &[i32] = &arr[1..3]; let from_vec: &[i32] = &v[1..3];
println!( "arr: {} bytes, the items themselves", size_of_val(&arr) ); println!( "v: {} bytes, a header; items: {} bytes", size_of_val(&v), size_of_val(&v[..]) ); println!( "from_arr: {} bytes, len {}", size_of_val(&from_arr), from_arr.len() ); println!( "from_vec: {} bytes, len {}", size_of_val(&from_vec), from_vec.len() ); println!( "from_arr points {} bytes into arr", from_arr.as_ptr() as usize - arr.as_ptr() as usize ); println!( "from_vec points {} bytes into v's buffer", from_vec.as_ptr() as usize - v.as_ptr() as usize );}arr: 16 bytes, the items themselves v: 24 bytes, a header; items: 16 bytes from_arr: 16 bytes, len 2 from_vec: 16 bytes, len 2 from_arr points 4 bytes into arr from_vec points 4 bytes into v's buffer
On this 64-bit machine a slice is 16 bytes (address and length), a Vec header 24 (adding the capacity), and an array is just its items.
Capacity and growth
Coming from Python, TypeScript or Java
Skip to the last paragraph here if you like. Python lists, JavaScript arrays and ArrayList also reallocate when full, but hide the capacity.
A Vec has a length (items held) and a capacity (items that fit in its block before it needs a bigger one). Watch both while pushing:
fn main() { let mut v: Vec<u32> = Vec::new(); println!("start: len {}, capacity {}", v.len(), v.capacity());
let mut last_ptr = v.as_ptr(); let mut last_cap = v.capacity(); for i in 1..=40 { v.push(i); if v.capacity() != last_cap { let what = if last_cap == 0 { "first buffer" } else if v.as_ptr() != last_ptr { "buffer moved" } else { "same address" }; println!("push #{i}: capacity {last_cap} -> {}, {what}", v.capacity()); last_cap = v.capacity(); last_ptr = v.as_ptr(); } } println!("end: len {}, capacity {}", v.len(), v.capacity());
let mut w: Vec<u32> = Vec::with_capacity(40); let before = w.as_ptr(); for i in 1..=40 { w.push(i); } println!( "with_capacity(40): after 40 pushes capacity {}, buffer moved: {}", w.capacity(), w.as_ptr() != before );
w.truncate(3); println!( "after truncate(3): len {}, capacity {}", w.len(), w.capacity() ); w.shrink_to_fit(); println!( "after shrink_to_fit: len {}, capacity {}", w.len(), w.capacity() );
let mut r = vec![1, 2, 3]; println!("vec![1, 2, 3]: capacity {}", r.capacity()); r.reserve(5); println!("after reserve(5): capacity {}", r.capacity());}start: len 0, capacity 0 push #1: capacity 0 -> 4, first buffer push #5: capacity 4 -> 8, same address push #9: capacity 8 -> 16, same address push #17: capacity 16 -> 32, same address push #33: capacity 32 -> 64, same address end: len 40, capacity 64 with_capacity(40): after 40 pushes capacity 40, buffer moved: false after truncate(3): len 3, capacity 40 after shrink_to_fit: len 3, capacity 3 vec![1, 2, 3]: capacity 3 after reserve(5): capacity 8
- An empty
Vechas capacity 0 and no buffer. The first push creates it. - Here the capacity went 4, 8, 16, 32, 64. Rust does not promise that: the docs say
Vec“does not guarantee any particular growth strategy when reallocating when full”, only thatpushis amortised O(1). The first size depends on the item size:
fn main() { let mut words: Vec<u32> = Vec::new(); words.push(1); println!("first push, Vec<u32>: capacity {}", words.capacity());
let mut bytes: Vec<u8> = Vec::new(); bytes.push(1); println!("first push, Vec<u8>: capacity {}", bytes.capacity());
let mut blocks: Vec<[u8; 2000]> = Vec::new(); blocks.push([0; 2000]); println!( "first push, Vec<[u8; 2000]>: capacity {}", blocks.capacity() );}first push, Vec<u32>: capacity 4 first push, Vec<u8>: capacity 8 first push, Vec<[u8; 2000]>: capacity 1
- Whether the items move depends on the allocator, the part of the system that hands out heap blocks. It can sometimes make a block longer where it stands. On the runner (Linux) no push moved an existing buffer (
same address); on another system you may seebuffer movedon every line. When a block cannot be extended, the items are copied into a new one and the old one is freed:
old and new would be the same address and nothing would be freed.with_capacity(40) never moves, because the room was reserved first (the docs recommend it when you know the size). truncate and clear keep the memory; shrink_to_fit asks for capacity to match the length, and the docs say the result depends on the allocator. reserve(extra) makes room for extra items beyond the length: vec![1, 2, 3] with reserve(5) has capacity 8, not 5.
The compiler cannot know whether a push will move the items, so it treats every push as one that might. That is why a reference into a Vec cannot survive a push: Mutable borrows draws the dangling reference this would leave.
Coming from Go
Go’s growth depends on the item type. For []int on this runner’s Go the numbers match Vec<u32>’s, but []string starts at 2 and []byte at 32. The spec promises no rule, so other Go versions may differ. The header is 24 bytes, the same three words as a Vec:
package main
import ( "fmt" "runtime" "unsafe")
func firstAppend[T any](name string) { var s []T s = append(s, *new(T)) fmt.Printf("first append to a []%s: cap %d\n", name, cap(s))}
func main() { fmt.Println(runtime.Version()) var v []int fmt.Println("size of a slice header:", unsafe.Sizeof(v), "bytes") fmt.Printf("start: len %d, cap %d\n", len(v), cap(v)) last := cap(v) for i := 1; i <= 40; i++ { v = append(v, i) if cap(v) != last { fmt.Printf("append #%d: cap %d -> %d\n", i, last, cap(v)) last = cap(v) } } firstAppend[string]("string") firstAppend[byte]("byte")}go1.27.3 size of a slice header: 24 bytes start: len 0, cap 0 append #1: cap 0 -> 4 append #5: cap 4 -> 8 append #9: cap 8 -> 16 append #17: cap 16 -> 32 append #33: cap 32 -> 64 first append to a []string: cap 2 first append to a []byte: cap 32
Coming from C++
std::vector grows with its own numbers, and C++’s reserve(n) is a total where Rust’s reserve(extra) is relative:
#include <iostream>#include <vector>
int main() { std::vector<int> v; std::size_t last = v.capacity(); std::cout << "start: capacity " << last << "\n"; for (int i = 1; i <= 40; i++) { v.push_back(i); if (v.capacity() != last) { std::cout << "push_back #" << i << ": capacity " << last << " -> " << v.capacity() << "\n"; last = v.capacity(); } }
std::vector<int> w{1, 2, 3}; w.reserve(5); std::cout << "{1, 2, 3}.reserve(5): capacity " << w.capacity() << "\n";}start: capacity 0
push_back #1: capacity 0 -> 1
push_back #2: capacity 1 -> 2
push_back #3: capacity 2 -> 4
push_back #5: capacity 4 -> 8
push_back #9: capacity 8 -> 16
push_back #17: capacity 16 -> 32
push_back #33: capacity 32 -> 64
{1, 2, 3}.reserve(5): capacity 5Also: std::vector always gets a new block when it grows (std::allocator cannot extend one), where Rust’s allocator may extend in place, and a Rust move is a plain byte copy with no move constructor to throw. And Rust rejects a push even when it would fit, while a reference is alive, though in C++ “reserve, take pointers, push_back up to the reserve” is legal:
fn main() { let mut v = Vec::with_capacity(10); v.push(1); let first = &v[0]; v.push(2); // there is room, so nothing would move println!("first: {first}");}error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable --> src/main.rs:5:5 | 4 | let first = &v[0]; | - immutable borrow occurs here 5 | v.push(2); // there is room, so nothing would move | ^^^^^^^^^ mutable borrow occurs here 6 | println!("first: {first}"); | ----- immutable borrow later used here
A Vec of Strings
A Vec<String> is two levels of heap: the buffer holds the String headers, and each points at its own text.
String is not promised, so it is drawn as ptr, len, cap.use std::mem::size_of;
fn main() { let names = vec![String::from("ana"), String::from("bo")];
println!("size_of::<String>(): {}", size_of::<String>()); println!("names: len {}, capacity {}", names.len(), names.capacity()); for (i, s) in names.iter().enumerate() { println!( "names[{i}]: \"{s}\" len {}, capacity {}", s.len(), s.capacity() ); } println!( "the items are {} bytes apart in the list's buffer", names[1..].as_ptr() as usize - names.as_ptr() as usize ); println!( "the first text is in a different block: {}", names[0].as_ptr() as usize != names.as_ptr() as usize );}size_of::<String>(): 24 names: len 2, capacity 2 names[0]: "ana" len 3, capacity 3 names[1]: "bo" len 2, capacity 2 the items are 24 bytes apart in the list's buffer the first text is in a different block: true
This is why names[0] cannot be taken out: the list owns the header, and dropping the list drops every header and its text. clone() copies both levels. remove and swap_remove hand the header to you and fix up the list.
Coming from C++
A short C++ string is stored inside the std::string object itself, so the second level of the picture exists only for longer text (Strings shows it).
Changing a list you are reading
Any call that needs &mut self is a writer: push, clear, remove, sort. A slice, a reference to one item and a loop over &v are readers, and readers and a writer cannot overlap (Mutable borrows):
fn main() { let mut v = vec![3, 9, 4]; let tail = &v[1..]; v.clear(); println!("tail is {tail:?}");}error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable --> src/main.rs:4:5 | 3 | let tail = &v[1..]; | - immutable borrow occurs here 4 | v.clear(); | ^^^^^^^^^ mutable borrow occurs here 5 | println!("tail is {tail:?}"); | ---- immutable borrow later used here
Take the slice after the change, or copy the part you need with to_vec() first. Mutable borrows shows the loop version and other languages. This is the biggest idea Rust adds to lists: the view and the list cannot both be live when the list changes.
Coming from C or C++
C++ calls the hazard iterator invalidation: push_back (everything, if it reallocates; the end iterator even if it does not, which is why a range-for that pushes is undefined behaviour), erase (from the erase point on) and clear (everything) invalidate iterators, references and spans into a vector. Using one afterwards is undefined behaviour, and nothing stops you. Mutable borrows runs a push_back in a loop under AddressSanitizer. Rust makes it a compile error.
Practice
Try it
Each exercise has checks in main. Open it in the Playground, replace the todo!(), and run it until it prints all checks passed. Plain for loops are enough.
Remove repeats, keep the order
Write unique: the items of a slice without repeats, in the order they first appeared. [3, 1, 3, 2, 1] becomes [3, 1, 2]. dedup removes only neighbouring repeats and sorting changes the order, so build a new Vec and ask it with contains (slow on a long list; the maps lesson has the fast way).
// Return the items of `items` with repeats removed, keeping the first of each// and the original order.fn unique(items: &[i32]) -> Vec<i32> { todo!("build a new Vec; push an item only if `contains` says it is not in it yet")}
fn main() { assert_eq!(unique(&[3, 1, 3, 2, 1]), vec![3, 1, 2]); assert_eq!(unique(&[2, 1, 2]), vec![2, 1]); // repeats that are not next to each other assert_eq!(unique(&[7, 7, 7]), vec![7]); assert_eq!(unique(&[1, 2, 3]), vec![1, 2, 3]); assert_eq!(unique(&[]), Vec::<i32>::new()); println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
// Return the items of `items` with repeats removed, keeping the first of each// and the original order.fn unique(items: &[i32]) -> Vec<i32> { let mut seen = Vec::new(); for item in items { if !seen.contains(item) { seen.push(*item); } } seen}
fn main() { assert_eq!(unique(&[3, 1, 3, 2, 1]), vec![3, 1, 2]); assert_eq!(unique(&[2, 1, 2]), vec![2, 1]); // repeats that are not next to each other assert_eq!(unique(&[7, 7, 7]), vec![7]); assert_eq!(unique(&[1, 2, 3]), vec![1, 2, 3]); assert_eq!(unique(&[]), Vec::<i32>::new()); println!("all checks passed");}all checks passed
Sums of neighbouring numbers
Write window_sums: the sum of every run of size neighbouring numbers. A slice shorter than size, an empty slice and size 0 give an empty Vec.
// The sum of every run of `size` neighbouring numbers, left to right.// If `size` is 0 or longer than the slice, there are no runs: return an empty Vec.fn window_sums(xs: &[i32], size: usize) -> Vec<i32> { todo!("`xs.windows(size)` gives each run as a slice, but it panics when size is 0")}
fn main() { assert_eq!(window_sums(&[1, 2, 3, 4, 5], 3), vec![6, 9, 12]); assert_eq!(window_sums(&[4, 5], 1), vec![4, 5]); assert_eq!(window_sums(&[1, 2, 3], 3), vec![6]); assert_eq!(window_sums(&[1, 2], 3), Vec::<i32>::new()); assert_eq!(window_sums(&[], 2), Vec::<i32>::new()); assert_eq!(window_sums(&[5, 6], 0), Vec::<i32>::new()); println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
// The sum of every run of `size` neighbouring numbers, left to right.// If `size` is 0 or longer than the slice, there are no runs: return an empty Vec.fn window_sums(xs: &[i32], size: usize) -> Vec<i32> { let mut sums = Vec::new(); if size == 0 { return sums; } for run in xs.windows(size) { let mut total = 0; for x in run { total += *x; } sums.push(total); } sums}
fn main() { assert_eq!(window_sums(&[1, 2, 3, 4, 5], 3), vec![6, 9, 12]); assert_eq!(window_sums(&[4, 5], 1), vec![4, 5]); assert_eq!(window_sums(&[1, 2, 3], 3), vec![6]); assert_eq!(window_sums(&[1, 2], 3), Vec::<i32>::new()); assert_eq!(window_sums(&[], 2), Vec::<i32>::new()); assert_eq!(window_sums(&[5, 6], 0), Vec::<i32>::new()); println!("all checks passed");}all checks passed
Reverse a &mut slice
Write reverse_in_place(xs: &mut [i32]) with swap, not reverse. The last check passes only the middle of a longer list: touch nothing outside the slice.
// Reverse the items of `xs` in place, without making a new Vec.// (Do not call `xs.reverse()`: the point is to write it with `swap`.)fn reverse_in_place(xs: &mut [i32]) { todo!("swap item i with item len - 1 - i, for the first half of the positions")}
fn main() { let mut even = vec![1, 2, 3, 4]; reverse_in_place(&mut even); assert_eq!(even, vec![4, 3, 2, 1]);
let mut odd = vec![1, 2, 3]; reverse_in_place(&mut odd); assert_eq!(odd, vec![3, 2, 1]);
let mut empty: Vec<i32> = Vec::new(); reverse_in_place(&mut empty); assert!(empty.is_empty());
let mut one = [9]; reverse_in_place(&mut one); assert_eq!(one, [9]);
// Only the middle of the list is a slice here; the ends must not move. let mut part = vec![1, 2, 3, 4, 5]; reverse_in_place(&mut part[1..4]); assert_eq!(part, vec![1, 4, 3, 2, 5]); println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
// Reverse the items of `xs` in place, without making a new Vec.// (Do not call `xs.reverse()`: the point is to write it with `swap`.)fn reverse_in_place(xs: &mut [i32]) { let len = xs.len(); for i in 0..len / 2 { xs.swap(i, len - 1 - i); }}
fn main() { let mut even = vec![1, 2, 3, 4]; reverse_in_place(&mut even); assert_eq!(even, vec![4, 3, 2, 1]);
let mut odd = vec![1, 2, 3]; reverse_in_place(&mut odd); assert_eq!(odd, vec![3, 2, 1]);
let mut empty: Vec<i32> = Vec::new(); reverse_in_place(&mut empty); assert!(empty.is_empty());
let mut one = [9]; reverse_in_place(&mut one); assert_eq!(one, [9]);
// Only the middle of the list is a slice here; the ends must not move. let mut part = vec![1, 2, 3, 4, 5]; reverse_in_place(&mut part[1..4]); assert_eq!(part, vec![1, 4, 3, 2, 5]); println!("all checks passed");}all checks passed
Take a name out of the list
Write take_longest(names: &mut Vec<String>) -> Option<String>: remove the longest name and return it. The first wins a tie; an empty list gives None. let name = names[i] will not compile, and names[i].clone() leaves a copy in the list, which the checks catch.
// Remove the longest name from the list and return it (the first one, if two// are equally long). Return None, and change nothing, if the list is empty.// The returned String must be the one that was in the list: no cloning.fn take_longest(names: &mut Vec<String>) -> Option<String> { todo!("find the position of the longest name, then take that one out of the list")}
fn main() { let mut names = vec![ String::from("ana"), String::from("beatrice"), String::from("bo"), String::from("cordelia"), ]; assert_eq!(take_longest(&mut names), Some(String::from("beatrice"))); assert_eq!(names, ["ana", "bo", "cordelia"]); // gone from the list, not copied assert_eq!(take_longest(&mut names), Some(String::from("cordelia"))); assert_eq!(names, ["ana", "bo"]); assert_eq!(take_longest(&mut names), Some(String::from("ana"))); assert_eq!(take_longest(&mut names), Some(String::from("bo"))); assert!(names.is_empty()); assert_eq!(take_longest(&mut names), None); println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
// Remove the longest name from the list and return it (the first one, if two// are equally long). Return None, and change nothing, if the list is empty.// The returned String must be the one that was in the list: no cloning.fn take_longest(names: &mut Vec<String>) -> Option<String> { let mut longest: Option<usize> = None; for (i, name) in names.iter().enumerate() { match longest { Some(best) if names[best].len() >= name.len() => {} _ => longest = Some(i), } } Some(names.remove(longest?))}
fn main() { let mut names = vec![ String::from("ana"), String::from("beatrice"), String::from("bo"), String::from("cordelia"), ]; assert_eq!(take_longest(&mut names), Some(String::from("beatrice"))); assert_eq!(names, ["ana", "bo", "cordelia"]); // gone from the list, not copied assert_eq!(take_longest(&mut names), Some(String::from("cordelia"))); assert_eq!(names, ["ana", "bo"]); assert_eq!(take_longest(&mut names), Some(String::from("ana"))); assert_eq!(take_longest(&mut names), Some(String::from("bo"))); assert!(names.is_empty()); assert_eq!(take_longest(&mut names), None); println!("all checks passed");}all checks passed
Read more
Vec: every method, and the sections on slicing and capacity and reallocation.- Slices and arrays in the standard library docs:
sort,binary_search,windows,chunks,concatand the rest are slice methods. - The Rust Book, chapter 8.1: Storing Lists of Values with Vectors
- Rust by Example: Vectors and Arrays and Slices
Learn Rust is an independent project, not affiliated with the Rust Project or the Rust Foundation. Code and text are under the MIT licence.Source on GitHub.