Skip to content
Learn Rust
Theme

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

Rustsrc/main.rsRun ↗
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());
}
$ cargo run
[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))
}
$ go run main.go
[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}")
}
$ kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar
[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)}")
$ python3 main.py
[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 view
console.log(v, w, part, `len=${w.length}`);
$ tsc --strict --module nodenext --types node main.ts && node main.js
[ 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());
}
}
$ javac Main.java && java Main
[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 expression
w.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}");
$ dotnet run
[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 capacity
struct 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;
}
$ gcc -Wall -Wextra main.c -o main && ./main
[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";
}
$ g++ -std=c++20 -Wall -Wextra main.cpp -o main && ./main
[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 a Vec<i32>, but for a Vec<String> it is a compile error: borrow with &v[0] or copy with v[0].clone().
  • v.sort() needs items that can always be ranked, and f64 cannot (NaN). For floats, write v.sort_by(|a, b| a.total_cmp(b)).
Gotcha

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.
Rustsrc/main.rs✗ won't compile
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))
}
$ go run main.go✓ runs
ana of 2
fun main() {
val names = mutableListOf("ana", "bo")
val first = names[0]
println("$first of ${names.size}")
}
$ kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar✓ runs
ana of 2
names = ["ana", "bo"]
first = names[0]
print(f"{first} of {len(names)}")
$ python3 main.py✓ runs
ana of 2
const names = ["ana", "bo"];
const first = names[0];
console.log(`${first} of ${names.length}`);
$ tsc --strict --module nodenext --types node main.ts && node main.js✓ runs
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());
}
}
$ javac Main.java && java Main✓ runs
ana of 2
List<string> names = ["ana", "bo"];
var first = names[0];
Console.WriteLine($"{first} of {names.Count}");
$ dotnet run✓ runs
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;
}
$ gcc -Wall -Wextra main.c -o main && ./main✓ runs
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";
}
$ g++ -std=c++20 -Wall -Wextra main.cpp -o main && ./main✓ runs
ana of 2
What does the compiler say? Guess first, then open.
$ cargo buildWhat is E0507?
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();
  |                         ++++++++
What happens in Go

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.

What happens in Kotlin

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.

What happens in Python

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.

What happens in TypeScript

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.

What happens in Java

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.

What happens in C#

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.

What happens in C

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.

What happens in C++

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.

What Rust sees

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.

Why Rust says no

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.

Rust, fixedsrc/main.rsRun ↗
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());
}
$ cargo run
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.

Rustsrc/main.rsRun ↗
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:?}");
}
$ cargo run
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 of 0.
  • 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 a push.
  • with_capacity(n) is empty with room for at least n items (capacity and growth).
  • collect() turns many things into a Vec (iterators).

{:?} prints a whole list; plain {} is not defined for lists.

Adding, reading and removing

Rustsrc/main.rsRun ↗
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()
);
}
$ cargo run
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
  • push adds at the end and pop takes the last item off. pop returns an Option, because the list may be empty.
  • v[1] reads (and v[1] = 25 writes) the item at position 1. The first item is position 0, and the Vec must be let mut to write.
  • get(i), first() and last() also return an Option: 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:

Rustsrc/main.rs! panics at runtimeRun ↗
fn main() {
let v = vec![10, 20, 30];
println!("v has {} items", v.len());
let i = 10;
println!("v[{i}] is {}", v[i]);
}
$ cargo run
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:

Rustsrc/main.rs✗ won't compile
fn main() {
let v = vec![10, 20, 30];
let i: i32 = 1;
println!("{}", v[i]);
}
$ cargo buildWhat is E0277?
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

Rustsrc/main.rsRun ↗
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:?}");
}
$ cargo run
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:

Rustsrc/main.rs✗ won't compile
fn main() {
let mut v = vec![1, 2];
let w = vec![3, 4];
v.extend(w);
println!("{v:?} {w:?}");
}
$ cargo buildWhat is E0382?
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

Rustsrc/main.rsRun ↗
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.
}
$ cargo run
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 &scores borrows the list and gives a &i32 per item. The list is still yours afterwards.
  • for s in &mut scores gives &mut i32, so *s += 5 changes 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, including sum, min and max (iterator methods, not Vec methods); looping over collections covers the three loop forms.

Sorting and searching

Rustsrc/main.rsRun ↗
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));
}
$ cargo run
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 an Ordering: Less, Equal or Greater. a.cmp(b) gives one, so b.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 finds x. binary_search(&x) looks at far fewer on a long list but needs a sorted one; otherwise its answer means nothing. It returns a Result (errors): Ok(position), or Err(position) where x could be inserted to keep the order. With equal items, any of them may be returned.

Sorting floats

f64 cannot use plain sort():

Rustsrc/main.rs✗ won't compile
fn main() {
let mut temps = vec![21.5, 19.0, 23.25];
temps.sort();
println!("{temps:?}");
}
$ cargo buildWhat is E0277?
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:

Rustsrc/main.rsRun ↗
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:?}");
}
$ cargo run
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.

Rustsrc/main.rsRun ↗
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:?}");
}
$ cargo run
&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, like v[9]. v.get(3..9) returns None.
  • split_at, windows and chunks are 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.
  • concat glues a list of lists into one, join(sep) does it with something in between (strings too), and to_vec() copies a slice into a new Vec.

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:

Rustsrc/main.rsRun ↗
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:?}");
}
$ cargo run
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.

Rustsrc/main.rsRun ↗
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:?}");
}
$ cargo run
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:

Rustsrc/main.rs✗ won't compile
fn main() {
let names = [String::from("ana"), String::from("bo")];
let other = names;
println!("{names:?} {other:?}");
}
$ cargo buildWhat is E0382?
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:

Rustsrc/main.rs✗ won't compile
fn main() {
let a = [10, 20, 30];
println!("{}", a[5]);
}
$ cargo build
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.

Rustsrc/main.rs! panics at runtimeRun ↗
fn main() {
let a = [10, 20, 30];
let i = a.len() + 2;
println!("a[{i}] = {}", a[i]);
}
$ cargo run
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.

Rustsrc/main.rsRun ↗
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());
}
}
$ cargo run
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:

Rustsrc/main.rsRun ↗
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:?}");
}
$ cargo run
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.)

let arr = [10, 20, 30, 40]; let v = vec![10, 20, 30, 40];
STACKHEAParr: [i32; 4]10203040a: &[i32]ptrlen 2a = &arr[1..3]: starts 4 bytes into arrv: Vec<i32>ptrlen 4cap 4b: &[i32]ptrlen 2b = &v[1..3]: starts 4 bytes into the bufferv's buffer10203040
The slices a and b have the same type and size, whether they look at the stack or the heap. Neither owns the items.
Rustsrc/main.rsRun ↗
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
);
}
$ cargo run
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

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:

Rustsrc/main.rsRun ↗
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());
}
$ cargo run
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 Vec has 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 that push is amortised O(1). The first size depends on the item size:
Rustsrc/main.rsRun ↗
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()
);
}
$ cargo run
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 see buffer moved on every line. When a block cannot be extended, the items are copied into a new one and the old one is freed:
v.push(5); // v: Vec<u32> is full, len 4, cap 4
STACKHEAPv: Vec<u32>ptrlen 5cap 8v's new buffer, room for 812345old buffer, room for 4 (freed)1234
The four items were copied across, the fifth was written, and three cells of spare room remain. If the allocator could extend the block in place, 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.

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.

let names = vec![String::from("ana"), String::from("bo")];
STACKHEAPnames: Vec<String>ptrlen 2cap 2the list buffer: 2 × String, 24 bytes eachptr, len 3, cap 3ptr, len 2, cap 23 bytesana2 bytesbo
The order of the three fields inside a String is not promised, so it is drawn as ptr, len, cap.
Rustsrc/main.rsRun ↗
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
);
}
$ cargo run
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.

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):

Rustsrc/main.rs✗ won't compile
fn main() {
let mut v = vec![3, 9, 4];
let tail = &v[1..];
v.clear();
println!("tail is {tail:?}");
}
$ cargo buildWhat is E0502?
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.

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).

Starterexercises/unique.rs
// 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
One solutionexercises/unique.solution.rsRun ↗
// 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");
}
$ cargo run
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.

Starterexercises/window-sums.rs
// 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
One solutionexercises/window-sums.solution.rsRun ↗
// 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");
}
$ cargo run
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.

Starterexercises/reverse-in-place.rs
// 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
One solutionexercises/reverse-in-place.solution.rsRun ↗
// 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");
}
$ cargo run
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.

Starterexercises/take-longest.rs
// 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
One solutionexercises/take-longest.solution.rsRun ↗
// 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");
}
$ cargo run
all checks passed

Read more