Rust Cheatsheet
Fundamental trait
fmt
Debug
For debug output, combine with {:?} or {:#?}(pretty format).
#[derive(Debug)] struct Point { x: f64, y: f64, } fn main() { let p = Point {x: 1.0, y: 2.0}; println!("{:?}", p); // Point {x: 1.0, y: 2.0} println!("{:#?}", p); // pretty format with multiline }
Or impl it manually which we could control the format of output.
use std::fmt; struct Color(u8, u8, u8); impl fmt::Debug for Color { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "#{:02X}{:02X}{:02X}", self.0, self.1, self.2) } } fn main() { let c = Color(255, 128, 0); println!("{:?}", c); // #FF8000 }
Display
For output information towards to user. Format with {}. Can not impl by derive, only impl manually.
use std::fmt; struct Temperature { celsius: f64, } impl fmt::Display for Temperature { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{:.1}°C", self.celsius) } } fn main() { let t = Temperature { celsius: 36.7}; println!("{}", t); // 36.7°C // will auto impl ToString when impl Display let s: String = t.to_string(); }
blanket impl: any type that impl Display will blanket impl ToString.
cmp
PartialEq / Eq
Use for == and != comparation. Eq provide extra reflexivity a==a always true.
#[derive(Debug, PartialEq)] struct Point { x: f64, y: f64, } fn main() { let a = Point {x: 1.0, y: 2.0 }; let b = Point {x: 1.0, y: 2.0 }; let c = Point {x: 1.0, y: 4.0 }; assert!(a == b); // true assert!(a != c); // true }
f64 can not impl Eq.
fn main() { let nan = f64::NAN; println!("{}", nan == nan); // false NAN != NAN }
Custom comparation:
struct CaseInsensitive(String); impl PartialEq for CaseInsensitive { fn eq(&self, other: &Self) -> bool { self.0.to_lowercase() == other.0.to_lowercase() } } fn main() { let a = CaseInsensitive("Hello".into()); let b = CaseInsensitive("hello".into()); assert!(a == b); // true }
PartialOrd / Ord
PartialOrd: use for <, >, <=, >= comparation. The comparation result could not exist.
Ord: all comparation, Must impl Eq first!
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] struct Score { // First compare grade then name grade: u32, name: String, } fn main() { let mut scores = vec![ Score { grade: 90, name: "Alice".into() }, Score { grade: 85, name: "Bob".into() }, Score { grade: 90, name: "Aaron".into() }, ]; scores.sort(); // Need impl Ord // Result: Bob(85), Aaron(90), Alice(90) }
Manually impl:
use std::cmp::Ordering; #[derive(Debug, PartialEq, Eq)] struct Student { name: String, score: u32, } impl PartialOrd for Student { fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) } } impl Ord for Student { fn cmp(&self, other: &Self) -> Ordering { // descend by score then ascend by name other.score.cmp(&self.score).then(self.name.cmp(&other.name)) } }
Copy / Clone
Clone
Explicit deep copy by calling .clone() method.
#[derive(Debug, Clone)] struct Config { name: String, // String already impl Clone (include heap data) values: Vec<i32>, // Vec also does this } fn main() { let c1 = Config { name: "prod".into(), values: vec![1, 2, 3], }; let c2 = c1.clone(); // deep copy! // c1 and c2 are absolutely two different objects. println!("{:?}", c1); println!("{:?}", c2); }
Could manually impl this triat:
#[derive(Debug)] struct Connection { url: String, id: u64, // generate new id via .clone() } static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); impl Clone for Connection { fn clone(&self) -> Self { Self { url: self.url.clone(), id: COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst), } } }
Copy
Implicit bitwise copy, not transfer ownship by assignment or parameter pass. Must impl Clone First!
#[derive(Debug, Clone, Copy)] struct Point { x: f64, y: f64, } fn main() { let a = Point {x: 1.0, y: 3.0}; let b = a; // Copy, bitwise, a still valid. let c = a; println!("{:?} {:?} {:?}", a, b, c); }
All scalar type could impl Copy, but the type which own its heap data can not impl Copy. Since
bitwise only copy the stack memory, will cause two variable pointer to same heap data.(Double free).
String, Vec<T>, Box<T> are all own heap data.
Copy vs Clone:
- Copy is managed by compiler, bitwise(memcpy), zero cost.
- Clone is explicitly call by programmer, could custom its implementation, could cost.
Default
Provide default value for type.
#[derive(Debug, Default)] struct Config { host: String, // "" port: u16, // 0 debug: bool, // false max_conn: usize, // 0 } fn main() { // all set default let c1 = Config::default(); // partial default let c2 = Config { port: 8080, debug: true, ..Default::default() }; }
Hash
Use for key of HashMap or HashSet.
use std::collections::HashMap; #[derive(Debug, PartialEq, Eq, Hash)] struct StudentId { year: u32, number: u32, } fn main() { let mut grades: HashMap<StudentId, f64> = HashMap::new(); grades.insert(StudentId{year: 2024, number: 1}, 95.0); grades.insert(StudentId{year: 2024, number: 2}, 85.0); let id = StudentId {year: 2024, number: 1}; println!("{:?}", grades.get(&id)); // Some(95.0) }
If a \== b, then must hash(a) == hash(b).
Type Convert
From / Into (TryFrom / TroInto)
Transfer bwteen values. Impl one will blanket impl another.
#[derive(Debug)] struct Meters(f64); #[derive(Debug)] struct Kilometers(f64); impl From<Kilometers> for Meters { fn from(km: Kilometers) -> Self { Meters(km * 1000.0) } } fn main() { let km = Kilometers(5.0); let m = Meters::from(Kilometers(5.0)); let m = km.into(); }
stdlib:
&str -> String i32 -> i64 Vec<u8> -> String
common used for error handle:
use std::io; use std::num::ParseIntError; #[derive(Debug)] enum AppError { Io(io::Error), Parse(ParseIntError), } impl From<io::Error> for AppError { fn from(e: io::Error) -> Self { AppError::Io(e) } } impl From<ParseIntError> for AppError { fn from(e: ParseIntError) -> Self { AppError::Parse(e) } } fn read_number(path: &str) -> Result<i32, AppError> { let content = std::fs::read_to_string(path)?; // auto into let num = content.trim().parse::<i32>()?; // auto into Ok(num) }
AsRef / AsMut
Zero cost for reference inner data for certain type. Or accept multiple type that looks like the same type.
Common usage:
pub fn read_to_string<P: AsRef<Path>>(path: P) -> io::Result<String>; // &str // String // &Path // PathBuf
This trait simplify the the process of type convert.
Deref / DerefMut
Use for smart pointer type that allow automatically dereference.
use std::ops::Deref; struct MyBox<T>(T); impl<T> Deref for MyBox<T> { type target = T; fn deref(&self) -> &T { &self.0 } } fn main() { let x = MyBox(40); assert_eq!(*x, 40); let s = MyBox(String::from("Hello")); fn takes_str(s: &str) { println!("{}", s); } takes_str(&s); // compiler automatically dereference }
stdlib:
// Box<T> -> T // String -> str // Vec<T> -> [T] // Arc<T> -> T // Rc<T> -> T // MuteGuard<T> -> T
Operator Overload (ops)
Add / Sub / Mul
use std::ops::Add; #[derive(Debug, Clone, Copy)] struct Vec2 { x: f64, y: f64, } impl Add for Vec2 { type Output = Vec2; fn add(self, other: Vec2) -> Vec2 { Vec2 { x: self.x + other.x, y: self.y + other.y, } } } impl Add<f64> for Vec2 { type Output = Vec2; fn add(self, scalar: f64) -> Vec2 { Vec2 { x: self.x + scalar, y: self.y + scalar, } } } fn main() { let a = Vec2 {x: 1.0, y: 2.0 }; let b = Vec2 {x: 2.0, y: 3.0 }; let c = a + b; let d = a + 10.0; }
Index / IndexMut
use [] access.
use std::ops::Index; #[derive(Debug)] struct Matrix { data: Vec<Vec<f64>>, } impl Index<(usize, usize)> for Matrix { type Output = f64; fn index(&self, idx: (usize, usize)) -> &f64 { &self.data[idx.0][idx.1] } } fn main() { let m = Matrix { data: vec![ vec![1.0, 2.0], vec![2.0, 3.0], ], }; println!("{}", m[(1, 1)]); // 3.0 }
Iterator
Iterator
Only need impl next() method.
struct CountDown { value: u32, } impl CountDown { fn new(start: u32) -> Self { CountDown { value: start } } } impl Iterator for CountDown { type Item = u32; fn next(&mut self) -> Option<u32> { if self.value == 0 { None } else { self.value -= 1; Some(self.value + 1) } } } fn main() { for n in CountDown::new(5) { // NOTE: for loop inner call into_iter which is impl by IntoIterator, but All Iterator auto impl that. println!("{}", n); } }
IntoIterator
for loop implementation is call .into_iter().
| relationship | comment |
| Iterator | what type that will produce items by next() |
| IntoIterator | what type could become Iterator by intoiter() |
| Iterator->IntoIterator | auto impl (blanket impl) |
| IntoIterator->Iterator | NO! |
| for loop | call IntoIterator |
| function parameter | priority for IntoIterator(more flexible) |
Closure
Fn / FnMut / FnOnce
The difference between these is how to capture the variables.
// FnOnce consume the variable, call only once fn call_once(f: impl FnOnce() -> String) -> String { f() } // FnMut capture with mut reference fn call_mut(mut f: impl FnMut()) { f(); f(); } // Fn capture with reference fn call_fn(f: impl Fn() -> i32) -> i32 { f() + f() } fn main() { let x = 1000; // read reference let add = |y| x + y; // impl Fn println!("{}", call_fn(|| add(5))); let mut count = 0; let mut increment = || { count += 1; }; call_mut(&mut increment); let name = String::from("Alice"); let greet = move || format!("Hello, {}!", name); println!("{}", call_once(greet)); // greet can not call again }
inherit:
FnOnce(most general) ↑ FnMut(subtrait) ↑ Fn(subtrait) So: we could pass Fn and FnMut to which require FnOnce, but only Fn that require Fn.
Error Handle
Error
Need also impl Debug and Display.
use std::fmt; use std::error::Error; #[derive(Debug)] enum DatabaseError { ConnectionFailed(String), QueryFailed { query: String, reson: String }, Timeout, } impl fmt::Display for DatabaseError { fn fmt(&self, f: fmt::Formatter) -> fmt::Result { match self { Self::ConnectionFailed(addr) => { write!(f, "Could not connection to database: {}", addr), } Self::QueryFailed {query, reason} => { write!(f, "Query failed {}: {}", query, reason), } Self::Timeout => { write!(f, "Database Timeout"), } } } } impl Error for DatabaseError {} // source() return None default
Lifetime
Drop
Call automatically when that type's instance leave out the scope.
struct TempFile { path: String, } impl TempFile { fn new(path: &str) -> Self { Self {path: path.into()} } } impl Drop for TempFile { fn drop(&mut self) { println!("delete tempfile: {}", self.path); // Do actually delete } }
Any type that impl Drop can not impl Copy too. Cause compiler does not know when to call drop.
Borrow / ToOwned
- Borrow: Another way to get reference, but make sure that Hash and Eq and Ord consistency.
- ToOwned: create ownship from reference. (more general clone)
use std::collections::HashMap; use std::borrow::Borrow; fn main() { let mut map: HashMap<String, i32> = HashMap::new(); map.insert("hello".to_string(), 64); // the parameter of HashMap::get use Borrow: // fn get<Q>(&self, k: &Q) -> Option<&V> where K: Borrow<Q> // String: Borrow<str>, so can use &str for query let val = map.get("hello"); let s: &str = "hello"; let owned: String = s.to_owned(); // Convension not Rule // hash(T) == hash(T.borrowed) // (T == other) <==> (borrowed == other) // T.cmp(..) has same order with T.borrowed.cmp(..) }
Concurrence
- Most type automatically impl
Send+Sync. - No
Send: Rc<T> (reference count operation is not atomic). - No
Sync: Cell<T>, RefCell<T>