The Dynamic Ones
Oversimplification warning: This chapter is especially loose with the truth. What we cover here is an extreme simplification of core Rust concepts called "ownership," "move semantics," and "borrowing." For the real deal, see The Book, Chapter 4.
What Are "Dynamic Things"
Rust types come in 2 flavors (oversimplified):
| Kind | Examples | Characteristics |
|---|---|---|
| Fixed-size things | i32, f64, bool, char |
They copy automatically. No worries |
| Dynamic-size things | String, Vec<T> |
Some special rules apply |
i32 and f64 are always 4 bytes or 8 bytes -- fixed size. They get copied automatically when you assign or pass them:
fn main() {
let a = 42;
let b = a; // copied
println!("{}", a); // OK! a is still usable
}
String and Vec Are Special
String and Vec can change in length. They have special rules:
fn main() {
let a = String::from("hello");
let b = a; // a's contents "move" to b
// println!("{}", a); // Compile error! a is no longer usable
println!("{}", b); // OK
}
Why? -- Don't worry about it. Just remember: "when you hand off a dynamic thing, it disappears from the original."
Same with function calls:
fn print_it(s: String) {
println!("{}", s);
}
fn main() {
let name = String::from("Alice");
print_it(name);
// println!("{}", name); // Error! name is gone
}
The Fix: clone
When in doubt, .clone() it. A copy is created so the original stays usable:
fn main() {
let a = String::from("hello");
let b = a.clone(); // make a copy
println!("{}", a); // OK!
println!("{}", b); // OK!
}
fn print_it(s: String) {
println!("{}", s);
}
fn main() {
let name = String::from("Alice");
print_it(name.clone()); // pass a copy
println!("{}", name); // OK!
}
"Isn't
.clone()a waste of memory?" -- Yes. But don't worry about it now. If it works, it works. Optimize when performance actually becomes a problem.
Vec: A Dynamic Array
Vec is an array that can change in length:
fn main() {
// Various ways to create one
let mut numbers: Vec<i32> = Vec::new();
numbers.push(1);
numbers.push(2);
numbers.push(3);
// One-liner with a macro
let colors = vec!["red", "green", "blue"];
println!("{:?}", numbers); // [1, 2, 3]
println!("{:?}", colors); // ["red", "green", "blue"]
}
Common Operations
fn main() {
let mut v = vec![10, 20, 30, 40, 50];
// Length
println!("len: {}", v.len()); // 5
// Access (index is usize)
let i: usize = 0;
println!("first: {}", v[i]); // 10
// Add
v.push(60);
// Remove last
v.pop();
// Loop
for item in v.clone() {
println!("{}", item);
}
// v is still usable (because we cloned)
println!("{:?}", v);
}
for item in vconsumesv, making it unusable afterwards. Passv.clone()instead.
Iterator Methods
Vec comes with methods you can use instead of loops. If you've used map or filter on arrays in other languages, these will feel familiar.
First, you need to know closures (anonymous functions):
|x| x + 1 // takes x, returns x + 1
|x, y| x + y // two parameters
|x| { x * 2 } // braces are fine too
|x| x % 2 == 0 // returning a bool works too
Oversimplification warning: Closures have an important property called "capturing the environment," but for now think of them as "short functions you can write inline."
map: Transform Each Element
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = numbers
.iter()
.map(|x| x * 2)
.collect();
println!("{:?}", doubled); // [2, 4, 6, 8, 10]
}
.iter() produces each element, .map() transforms them, .collect() gathers them back into a Vec. This three-step combo is the basic pattern.
.iter()creates an iterator that "just peeks at" elements. The original Vec isn't consumed, so no clone needed.
filter: Keep Only Matching Elements
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6];
let evens: Vec<&i32> = numbers
.iter()
.filter(|x| *x % 2 == 0)
.collect();
println!("{:?}", evens); // [2, 4, 6]
}
filter's closure receives&&i32(a reference to a reference). Write**x, or use|&&x|for pattern matching, or*x... honestly it's confusing. Just follow the compiler errors and add/remove*until it works.
Chaining map and filter
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Keep only evens, then double them
let result: Vec<i32> = numbers
.iter()
.filter(|x| *x % 2 == 0)
.map(|x| x * 2)
.collect();
println!("{:?}", result); // [4, 8, 12, 16, 20]
}
Chain them with method calls. Easy to read.
enumerate: Loop with Index
fn main() {
let fruits = vec!["apple", "banana", "cherry"];
for (i, fruit) in fruits.iter().enumerate() {
println!("{}: {}", i, fruit);
}
// 0: apple
// 1: banana
// 2: cherry
}
enumerate() returns a tuple of (usize, &element).
find: First Matching Element
fn main() {
let numbers = vec![1, 3, 5, 8, 10, 13];
let first_even = numbers.iter().find(|x| *x % 2 == 0);
match first_even {
Some(n) => println!("found: {}", n), // found: 8
None => println!("not found"),
}
}
find returns an Option. If nothing matches, you get None.
any / all: Condition Checks
fn main() {
let numbers = vec![2, 4, 6, 8];
let has_odd = numbers.iter().any(|x| x % 2 != 0);
println!("has odd: {}", has_odd); // false
let all_positive = numbers.iter().all(|x| *x > 0);
println!("all positive: {}", all_positive); // true
}
sum / count
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let total: i32 = numbers.iter().sum();
println!("sum: {}", total); // 15
let how_many = numbers.iter().filter(|x| *x > 3).count();
println!("count > 3: {}", how_many); // 2
}
sum() often needs a type annotation (: i32). If the compiler says "I don't know what type to sum into," just add one.
Summary: Common Iterator Methods
| Method | What it does | Returns |
|---|---|---|
.iter() |
Creates an iterator (doesn't consume the original) | Iterator |
.map(|x| ...) |
Transforms each element | Iterator |
.filter(|x| ...) |
Keeps only matching elements | Iterator |
.collect() |
Converts an iterator into a Vec, etc. | Vec, etc. |
.enumerate() |
Attaches an index | Iterator |
.find(|x| ...) |
First matching element | Option |
.any(|x| ...) |
Whether any element matches | bool |
.all(|x| ...) |
Whether all elements match | bool |
.sum() |
Total | Numeric type |
.count() |
Count elements | usize |
You can do the same with for loops, but iterator methods make your intent clearer. They're used constantly in Rust, so get comfortable with them.
Vec and String Are Both "Dynamic Things"
fn main() {
let a = vec![1, 2, 3];
let b = a;
// println!("{:?}", a); // Error!
println!("{:?}", b); // OK
}
Same rules as String. Clone to fix it.
HashMap: Key-Value Pairs
Bonus. The equivalent of dictionaries or maps in other languages:
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert(String::from("Alice"), 100);
scores.insert(String::from("Bob"), 85);
// Retrieve
match scores.get("Alice") {
Some(score) => println!("Alice: {}", score),
None => println!("not found"),
}
// Loop
for (name, score) in scores.clone() {
println!("{}: {}", name, score);
}
}
Don't forget use std::collections::HashMap; at the top. get returns an Option -- we'll explain that in the next chapter.
Summary
i32,f64,bool-> They get copied. Don't worryString,Vec,HashMap-> They disappear when handed off. When in doubt,.clone()- Don't overthink the deeper reasons for now
Next chapter, we'll learn Option and Result, and that'll wrap up the Rust fundamentals. Then you'll be ready to build a calculator.