Rust Cheatsheet

Ownership and Borrowing

Use this Rust reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

The Three Rules of Ownership

  1. Each value has exactly one owner.
  2. When the owner goes out of scope, the value is dropped (memory freed).
  3. There can only be one owner at a time — assignment/passing moves the value.
{
    let s = String::from("hello"); // s owns the String
    // ... use s
}   // s goes out of scope → dropped here

Move Semantics

Types that are NOT Copy are moved on assignment or function call — the original binding becomes invalid.

let s1 = String::from("hello");
let s2 = s1;           // s1 is MOVED into s2
// println!("{}", s1); // compile error: value borrowed after move

fn takes_ownership(s: String) { /* s dropped here */ }
takes_ownership(s2);
// println!("{}", s2); // compile error

Copy Types

Types that implement Copy are duplicated on assignment — no move occurs.

let x: i32 = 5;
let y = x;    // x is copied; both x and y are valid
println!("{} {}", x, y);  // fine

Copy types: all primitive integers, floats, bool, char, (), tuples/arrays of Copy types, raw pointers, &T references. Not Copy: String, Vec, Box, most heap-allocated types.

Clone

Explicit deep copy with .clone():

let s1 = String::from("hello");
let s2 = s1.clone();   // heap data duplicated
println!("{} {}", s1, s2);  // both valid

Cloning is expensive — prefer borrows when possible.

References and Borrowing

A reference lets you use a value without taking ownership. Prefixed with &.

fn calculate_length(s: &String) -> usize {
    s.len()
}  // s is NOT dropped here; caller still owns it

let s1 = String::from("hello");
let len = calculate_length(&s1);  // borrow s1
println!("{} has length {}", s1, len);  // s1 still valid

Passing &value = borrowing. The function's parameter type must match (&Type).

Mutable References

fn change(s: &mut String) {
    s.push_str(", world");
}

let mut s = String::from("hello");
change(&mut s);   // pass mutable reference

Rules for mutable references: - You can have one &mut reference OR any number of & references — never both simultaneously. - Prevents data races at compile time.

let mut s = String::from("hello");

let r1 = &s;      // ok
let r2 = &s;      // ok — multiple immutable refs
// let r3 = &mut s; // ERROR: cannot borrow as mutable while immutable refs exist

println!("{} {}", r1, r2);  // r1, r2 last used here
let r3 = &mut s;   // ok now — r1/r2 no longer in use (Non-Lexical Lifetimes)
r3.push('!');

Dangling References (Prevented by Compiler)

// This does NOT compile:
fn dangle() -> &String {  // returns reference to dropped value
    let s = String::from("hello");
    &s   // s dropped at end of fn — dangling!
}

// Fix: return owned String
fn no_dangle() -> String {
    String::from("hello")
}

The Slice Type

Slices are references to a contiguous sequence within a collection — no ownership.

let s = String::from("hello world");
let hello = &s[0..5];   // &str slice, bytes 0..5
let world = &s[6..11];

// Range shorthand
let from_start = &s[..5];   // same as &s[0..5]
let to_end = &s[6..];       // same as &s[6..11]
let whole = &s[..];         // entire string

// String literals ARE &str slices (pointing into binary)
let lit: &str = "hello";

Array slices:

let a = [1, 2, 3, 4, 5];
let slice: &[i32] = &a[1..3];  // [2, 3]

Lifetimes

Lifetimes ensure references are valid as long as they're used. The compiler infers most lifetimes; annotation is needed when it cannot.

Lifetime annotation syntax — annotate with 'a:

// Without annotation: compiler can't determine which input the output borrows from
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

let s1 = String::from("long string");
let result;
{
    let s2 = String::from("xyz");
    result = longest(s1.as_str(), s2.as_str());
    println!("{}", result);  // ok — result used within s2's scope
}

Lifetime in structs:

struct Important<'a> {
    part: &'a str,   // struct cannot outlive the reference it holds
}

impl<'a> Important<'a> {
    fn announce(&self) -> &str {
        self.part
    }
}

Static lifetime — lives for the entire program:

let s: &'static str = "I live forever";

Lifetime Elision Rules

The compiler applies these rules so you don't always need to write lifetimes:

  1. Each reference parameter gets its own lifetime: fn f(x: &str, y: &str)fn f<'a,'b>(x: &'a str, y: &'b str)
  2. If there is exactly one input lifetime, it's assigned to all outputs.
  3. If one input is &self or &mut self, the self lifetime is assigned to all outputs.
// These are equivalent:
fn first_word(s: &str) -> &str { ... }
fn first_word<'a>(s: &'a str) -> &'a str { ... }

The Borrow Checker Summary

SituationAllowed?
Multiple &T at the same timeYes
One &mut T with no &TYes
&T and &mut T simultaneouslyNo
Multiple &mut T simultaneouslyNo
Reference outliving the valueNo
Moving out of borrowed referenceNo

Interior Mutability

When you need mutation through a shared reference (bypasses borrow checker at runtime):

use std::cell::RefCell;

let data = RefCell::new(vec![1, 2, 3]);
data.borrow_mut().push(4);       // runtime borrow check
println!("{:?}", data.borrow()); // [1, 2, 3, 4]

// Panics at runtime if borrow rules violated:
let _b1 = data.borrow();
// let _b2 = data.borrow_mut(); // would panic

Rc<RefCell<T>> — shared ownership with interior mutability (single thread):

use std::rc::Rc;
use std::cell::RefCell;

let shared = Rc::new(RefCell::new(0));
let clone1 = Rc::clone(&shared);
*clone1.borrow_mut() += 1;
println!("{}", shared.borrow()); // 1

For multi-thread: use Arc<Mutex<T>> or Arc<RwLock<T>>.

Smart Pointers and Ownership

TypeDescription
Box<T>Heap allocation, single owner
Rc<T>Reference-counted, multiple owners (single thread)
Arc<T>Atomic ref-counted, multiple owners (multi-thread)
RefCell<T>Interior mutability, runtime borrow checking (single thread)
Mutex<T>Interior mutability with locking (multi-thread)
RwLock<T>Multiple readers OR one writer (multi-thread)
Cell<T>Interior mutability for Copy types
let b = Box::new(5);
println!("{}", *b);   // deref coercion often implicit

let rc = Rc::new(String::from("shared"));
let rc2 = Rc::clone(&rc);
println!("count: {}", Rc::strong_count(&rc)); // 2

Drop Trait

Called automatically when value goes out of scope:

struct Resource;
impl Drop for Resource {
    fn drop(&mut self) {
        println!("Resource dropped!");
    }
}

// Force early drop:
let r = Resource;
drop(r);  // explicit drop; cannot use r after this