Values, Types and Ownership
Rust's type system is conventional. What is not conventional is ownership:
every value has exactly one owner, and when the owner goes out of scope the
value is dropped. No garbage collector, no manual free, and no way to use
memory after it is released - because the compiler will not let you.
fn main() {
let count = 42; // immutable by default
let mut total = 0; // mut to allow reassignment
let ratio: f64 = 0.5; // annotate when inference needs help
let name = "Ada"; // &str: a borrowed string slice
let owned = String::from("Ada"); // String: owns its buffer
total += count;
println!("{count} {total} {ratio} {name} {owned}");
println!("{:?}", (count, ratio, name)); // Debug formatting
println!("{:#?}", vec![1, 2, 3]); // pretty Debug
}
let without mut is the default, and it is the right default: most values
never need to change, and saying so lets the compiler help.
Moves: assignment gives the value away
For a type that owns memory - String, Vec<T>, a struct containing them -
assignment moves the value. The old name is no longer usable, which is what
makes a double free impossible.
fn main() {
let first = String::from("hello");
let second = first; // moved: `first` is no longer valid
// println!("{first}"); // error[E0382]: borrow of moved value
println!("{second}");
// Clone is the explicit, visible copy.
let third = second.clone();
println!("{second} and {third}");
// Small types that fit in a register are Copy: assignment duplicates them.
let a = 42;
let b = a;
println!("{a} {b}"); // both fine: i32 is Copy
// Passing to a function moves too, unless the type is Copy.
let text = String::from("given away");
consume(text);
// println!("{text}"); // error: value moved into consume
let number = 7;
consume_copy(number);
println!("{number} is still here");
}
fn consume(value: String) {
println!("consumed {value}");
}
fn consume_copy(value: i32) {
println!("copied {value}");
}
Borrowing: use without taking
A reference lets you read or modify a value without owning it. The rule the compiler enforces: either one mutable reference, or any number of immutable ones - never both at once.
fn main() {
let mut text = String::from("hello");
// Many readers at once is fine.
let r1 = &text;
let r2 = &text;
println!("{r1} {r2} ({} chars)", length(&text));
// One writer, and no readers while it lives.
let w = &mut text;
w.push_str(", world");
println!("{w}");
// The original owner can be used again once the borrow ends.
println!("{text}");
// This is what the rule prevents:
// let r = &text;
// let w = &mut text; // error: cannot borrow as mutable
// println!("{r}"); // because r is still in use here
}
fn length(value: &String) -> usize {
value.len() // borrowed, so the caller keeps it
}
That one rule eliminates data races at compile time, which is why Rust can be concurrent without a garbage collector.
&str and String
String owns a growable buffer. &str is a view into one - or into a literal
baked into the binary. Take &str in function parameters and you accept both.
fn main() {
let owned: String = String::from("Ada Lovelace");
let borrowed: &str = "Ada Lovelace";
let slice: &str = &owned[0..3];
println!("{}", greet(&owned)); // a String derefs to &str
println!("{}", greet(borrowed));
println!("{}", greet(slice));
let mut building = String::new();
for i in 1..=5 {
building.push_str(&i.to_string());
building.push(',');
}
println!("{building}");
// Strings are UTF-8, so indexing by byte is forbidden - the compiler will
// not let you write owned[0]. Iterate by char, or slice on a boundary.
let unicode = "héllo";
println!("{} bytes, {} chars", unicode.len(), unicode.chars().count());
for (i, c) in unicode.char_indices().take(3) {
println!(" byte {i}: {c}");
}
}
fn greet(name: &str) -> String {
format!("Hello, {name}")
}
Numbers, and the overflow the compiler makes you handle
fn main() {
let a: i32 = 7;
let b: i32 = 2;
println!("{} {} {}", a / b, a as f64 / b as f64, a % b);
// No implicit conversions: `as` is always explicit.
let big: i64 = a as i64 * 1_000_000_000;
println!("{big}");
// In debug builds, overflow panics. In release it wraps. Neither is silent
// if you use the explicit methods.
let max = i32::MAX;
println!("checked: {:?}", max.checked_add(1)); // None
println!("saturating: {}", max.saturating_add(1)); // i32::MAX
println!("wrapping: {}", max.wrapping_add(1)); // i32::MIN
let (value, overflowed) = max.overflowing_add(1);
println!("overflowing: {value} (overflowed: {overflowed})");
// Floats are binary approximations, here as everywhere.
println!("{}", 0.1 + 0.2);
println!("{}", (0.1f64 + 0.2 - 0.3).abs() < f64::EPSILON);
}
What to take away
- Values are immutable unless declared
mut, and that is the useful default. - Assignment moves an owning value; the old name stops compiling, not crashing.
Copytypes duplicate instead of moving;clone()is the explicit deep copy.- Borrowing: many
&T, or one&mut T, never both - which is what kills data races. - Take
&strin parameters and you accept bothStringand a literal. - Overflow panics in debug and wraps in release: use
checked_,saturating_orwrapping_.